13 Commits

Author SHA1 Message Date
AR 15 M4 6ce176f970 chore: close Sprint 4, open Sprint 5
- Sidebar: Hero/CEO-текст unlocked (not soon), Sprint 5 · v0.5.0
- SPRINTS.md: Sprint 4 marked  ЗАВЕРШЁН, Sprint 5 plan updated
- LLM_CONTEXT.md: v4.1 — cards/badges/alerts section 9b added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:19:38 +05:00
AR 15 M4 6c5b571884 feat(sprint4): add cards page — DoctorCard, NewsCard, ReviewCard, PriceCard, ServiceCard, badges/tags/alerts
- /components/cards — new page with 5 card types + badges/tags/alerts + LLM block v1.0
- DoctorCard: photo 110×160px placeholder, name, specialty, experience, outline button
- NewsCard: hover bg #eef4d1 + box-shadow (matches real site CSS), CSS class .bb-news-card
- ReviewCard: star rating 1-5 (SVG), text 4-line clamp, bg #eef4d1
- PriceCard: service + price + description + button, highlighted variant (blue border/bg)
- ServiceCard: emoji icon 48×48px, title, description, link
- Badges: 6 color variants (primary/success/warning/danger/neutral/outline)
- Tags: default/active state filters
- Alerts: 4 types (info/success/warning/error) with icons
- globals.css: .bb-news-card:hover, .bb-service-card:hover CSS rules (Sprint 4)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:09:22 +05:00
AR 15 M4 2b66fb9cc1 docs: update LLM_CONTEXT.md to v4.0 (Sprint 4 start)
- All 6 Oracal brand colors updated to exact catalog RGB values
- Contrast table recalculated for new hex values
- Color mapping notes updated (typography of differences)
- Logo color variants updated with new hex refs
- CSS vars section updated
- Transport hex refs updated
- Color rules updated (white on 073M/066M/050M/080M, dark on 053M/081M)
- Section 9a: button variants now match real site (primary/outline/teal/pill)
- Section 9a: form controls match real site (height 50px, border #ccc)
- Section 7: /components/cards added as Sprint 4 in progress
- History: version 4.0 added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:04:32 +05:00
AR 15 M4 3811c579fd fix(colors): update Oracal brand colors to exact catalog RGB values
053M: #7ecfca → #0089C3 (rgb 0,137,195)
073M: #5b7b87 → #53514E (rgb 83,81,78)
066M: #5bb5ad → #00818C (rgb 0,129,140)
050M: #1b4c72 → #1B2E5D (rgb 27,46,93)
081M: #c4a882 → #A8885C (rgb 168,136,92)
080M: #5c2e0e → #432F1E (rgb 67,47,30)

- Update globals.css CSS vars + sidebar active bg (→ #dff0fa)
- Update focus box-shadow rgba for new 053M blue
- colors/page.tsx: BRAND_COLORS, COLOR_MAPPING notes, CONTRAST_PAIRS
- LLM text v2.2: new hex/rgb, recalculated WCAG contrast ratios,
  updated rules (white text on 073M/066M/050M/080M, dark on 053M/081M)
- Info banner changed from ⚠️ "приблизительны" to ℹ "точные RGB"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 22:46:50 +05:00
AR 15 M4 2b9319e70d chore(sprint4): start Sprint 4 — cards
- Sidebar: Sprint 4 · v0.4.0, /components/cards removed from "soon"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 21:42:46 +05:00
AR 15 M4 d6610a288b chore: add offline photos + mark Sprint 3 complete in SPRINTS.md
- Add 27 offline photos (navigation + transport) that were missing from git
  but referenced by Sprint 2 pages
- SPRINTS.md: mark Sprint 3 as ЗАВЕРШЁН with actual results documented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 21:40:22 +05:00
AR 15 M4 77c9733144 fix(forms): update form controls to match real site oclinica.ru styling
- .bb-input: height 50px, padding 10px 12px, border 1px solid #ccc, border-radius 4px
  (matches entityform input[type=text] from perm.oclinica.ru)
- .bb-select: height 50px, padding 10px with arrow, same border/radius
- .bb-textarea: same border 1px #ccc, border-radius 4px (was 8px/1.5px teal)
- forms/page.tsx v2.0: added "Контекст применения" section with
  where-used table and realistic form mockup (bg #b8e6ed as on site),
  added "CSS с сайта" code block, updated LLM block to v2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 21:19:09 +05:00
AR 15 M4 c1731615ab fix(buttons): переделаны кнопки под реальный сайт oclinica.ru
Анализ CSS сайта (style.css темы clinic_bootstrap_mobile) выявил
4 реальных типа кнопок — заменены ранее придуманные варианты:

- primary  → коралловый #FFA39C + shadow (форм-сабмит «Запишите меня!»)
- outline  → белый + бежевая рамка #BF9975 («Записаться на прием»)
- teal     → бирюзовый #60959c («Позвонить»)
- pill     → кремовый #e9e4d4 + radius 25px («Заказать звонок»)

Удалены: secondary, ghost, danger (не существуют на реальном сайте)
Добавлен раздел «CSS с сайта» с точными значениями
Добавлена таблица «Где применяется» с реальными CSS-классами сайта
LLM-блок обновлён до v2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:41:27 +05:00
AR 15 M4 0855892643 feat(sprint3): кнопки, форм-контролы, LLM-блоки — Sprint 3 v0.3.0
- components/ui/Button.tsx — компонент Button (primary/secondary/ghost/danger, sm/md/lg, loading/disabled)
- components/ui/CodeCopy.tsx — компонент копирования кода (clipboard API)
- components/ui/Toggle.tsx — тумблер (client component, bb-toggle-track/thumb)
- globals.css — CSS-классы: bb-btn, bb-input/textarea/select, bb-checkbox/radio, bb-toggle, @keyframes bb-spin
- app/components/buttons/page.tsx — страница «Кнопки» (варианты, размеры, состояния, code copy, LLM-блок)
- app/components/forms/page.tsx — страница «Форм-контролы» (Input/Textarea/Select/Checkbox/Radio/Toggle, LLM-блок)
- foundation/logo/page.tsx — добавлен LLM-блок v1.0
- Sidebar: убраны «скоро» с Кнопок и Форм-контролов, версия Sprint 3 · v0.3.0
- docs/LLM_CONTEXT.md → версия 3.0, добавлена секция 9a с компонентами

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:20:41 +05:00
AR 15 M4 0198947c4e feat: LLM-блоки на страницах цветов и типографики + docs/LLM_CONTEXT.md
- Создан компонент components/llm/LlmBlock.tsx (use client):
  LlmBlock, LlmSection, LlmTable, LlmRules — переиспользуемый
  паттерн для LLM-спецификаций на всех страницах брендбука

- /foundation/colors: добавлен раздел «LLM-спецификация» (v2.1)
  · таблица фирменных цветов Oracal (7 шт.)
  · таблица цветов сайта oclinica.ru (11 шт., +3 новых: коралловый,
    светло-жёлтый, светло-зелёный)
  · таблица контрастности WCAG 2.1
  · правила применения ✓/✕
  · кнопка «Скопировать» — plain text для LLM

- /foundation/typography: добавлен раздел «LLM-спецификация» (v2.0)
  · таблица шрифтов DINPro vs Fira Sans
  · шкала DINPro (6 стилей)
  · шкала Fira Sans (11 стилей, включая letter-spacing)
  · применение по носителям + правила ✓/✕

- docs/LLM_CONTEXT.md: создан сводный машиночитаемый файл бренда
  Версия 2.1: все цвета, типографика, логотип, оффлайн, CSS-vars,
  правила — единый контекст для AI при работе с брендом клиники

- docs/TZ.md: добавлено требование ФТ-03-LLM
- docs/SPRINTS.md: задачи LLM-блоков в Sprint 3–8

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 19:59:58 +05:00
AR 15 M4 6648f16bcb feat(colors): блок соответствия Oracal → Сайт
Визуальное сравнение фирменной палитры с реальными цветами сайта:
- 4 пары с соответствием (053M↔#63bac3, 073M↔#60959c, 066M↔#52b4bd, 081M↔#bf9975)
- 2 Oracal-цвета без аналога на сайте (050M, 080M)
- 3 цвета только сайта без Oracal-кода (текст #464646, серый #949290, крем #e9e4d4)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 19:34:04 +05:00
AR 15 M4 761347ed85 feat(colors): раздел «Цвета с сайта» + документация источника CSS
- Добавлен массив WEB_COLORS (8 цветов из CSS темы oclinica.ru)
- Добавлен компонент WebColorCard с группой, счётчиком применений, HEX/RGB/HSL
- Новый раздел «Цвета с сайта» со ссылкой на CSS-источник в интерфейсе
- TZ.md: ОВ-07 — зафиксирован URL темы clinic_bootstrap_mobile и метод извлечения

Источник CSS: perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 19:29:27 +05:00
AR 15 M4 52acdc98e2 feat(sprint2): реальные фото из PDF, тайтлы страниц, убрана Печать
- Навигация: макет Кабинет 04 + карточка врача из PDF, фото дверей 13 и 31, указатели по этажам
- Транспорт: рендер макета трамвая из PDF вместо CSS-заглушки
- Тайтлы: единый формат «Раздел. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой» на всех страницах
- Сайдбар: убран пункт «Печать» (нет данных из брендбука)
- SPRINTS.md: обновлены фактические результаты Sprint 2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 19:14:17 +05:00
50 changed files with 3760 additions and 290 deletions
+573
View File
@@ -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>
);
}
+629
View File
@@ -0,0 +1,629 @@
import type { Metadata } from "next";
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-14">
<div className="mb-6">
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>{title}</h2>
{subtitle && <p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>{subtitle}</p>}
</div>
{children}
</section>
);
}
/* ─── Карточка врача ────────────────────────────────────────────────── */
function DoctorCard({
name, specialty, experience, photo,
}: {
name: string; specialty: string; experience: string; photo?: string;
}) {
return (
<div
className="flex gap-4 p-4 rounded-xl border bg-white transition-shadow"
style={{ borderColor: "var(--bb-border)" }}
>
{/* Фото */}
<div
className="shrink-0 rounded-lg overflow-hidden"
style={{ width: 110, height: 160, background: "#dff0fa" }}
>
{photo ? (
<img src={photo} alt={name} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
) : (
<div
className="w-full h-full flex flex-col items-center justify-center gap-1"
style={{ color: "var(--brand-053m)", opacity: 0.5 }}
>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
<span className="text-xs">фото</span>
</div>
)}
</div>
{/* Информация */}
<div className="flex flex-col justify-between min-w-0">
<div>
<h3 className="font-semibold text-sm leading-tight mb-1" style={{ color: "var(--bb-text)" }}>
{name}
</h3>
<p className="text-xs mb-2" style={{ color: "var(--bb-text-muted)" }}>{specialty}</p>
<p className="text-xs" style={{ color: "var(--brand-053m)" }}>{experience}</p>
</div>
<button className="bb-btn bb-btn-sm bb-btn-outline mt-3 self-start">
Записаться
</button>
</div>
</div>
);
}
/* ─── Карточка новости ──────────────────────────────────────────────── */
function NewsCard({
date, title, snippet, category,
}: {
date: string; title: string; snippet: string; category?: string;
}) {
return (
<div
className="bb-news-card rounded-xl border overflow-hidden cursor-pointer transition-all"
style={{ borderColor: "var(--bb-border)", background: "#fff" }}
>
{/* Превью */}
<div
className="h-36 flex items-center justify-center"
style={{ background: "#f0f9ff" }}
>
{category && (
<span
className="px-3 py-1 rounded-full text-xs font-semibold"
style={{ background: "var(--brand-053m)", color: "#fff" }}
>
{category}
</span>
)}
</div>
{/* Контент */}
<div className="p-4">
<p className="text-xs mb-2" style={{ color: "var(--bb-text-muted)" }}>{date}</p>
<h3 className="font-semibold text-sm leading-tight mb-2" style={{ color: "var(--bb-text)" }}>
{title}
</h3>
<p className="text-xs leading-relaxed mb-3" style={{ color: "var(--bb-text-muted)" }}>
{snippet}
</p>
<span className="text-xs font-medium" style={{ color: "var(--brand-053m)" }}>
Читать далее
</span>
</div>
</div>
);
}
/* ─── Карточка отзыва ───────────────────────────────────────────────── */
function ReviewCard({
author, date, text, rating, doctor,
}: {
author: string; date: string; text: string; rating: number; doctor?: string;
}) {
return (
<div
className="rounded-xl border p-5"
style={{ borderColor: "var(--bb-border)", background: "#eef4d1" }}
>
{/* Звёзды */}
<div className="flex gap-0.5 mb-3">
{Array.from({ length: 5 }, (_, i) => (
<svg key={i} width="16" height="16" viewBox="0 0 24 24" fill={i < rating ? "#f59e0b" : "none"} stroke="#f59e0b" strokeWidth="1.5">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
))}
<span className="text-xs ml-1 font-medium" style={{ color: "#92400e" }}>{rating}/5</span>
</div>
{/* Текст */}
<p
className="text-sm leading-relaxed mb-4"
style={{
color: "var(--bb-text)",
display: "-webkit-box",
WebkitLineClamp: 4,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{text}
</p>
{/* Автор */}
<div className="flex items-end justify-between">
<div>
<p className="text-sm font-semibold" style={{ color: "var(--bb-text)" }}>{author}</p>
{doctor && <p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>Врач: {doctor}</p>}
</div>
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{date}</p>
</div>
</div>
);
}
/* ─── Карточка цены ─────────────────────────────────────────────────── */
function PriceCard({
service, price, description, highlighted,
}: {
service: string; price: string; description?: string; highlighted?: boolean;
}) {
return (
<div
className="rounded-xl border p-5 flex flex-col gap-3"
style={{
borderColor: highlighted ? "var(--brand-053m)" : "var(--bb-border)",
background: highlighted ? "#f0f9ff" : "#fff",
}}
>
<div className="flex items-start justify-between gap-3">
<p className="text-sm font-medium flex-1" style={{ color: "var(--bb-text)" }}>{service}</p>
<p className="text-lg font-bold shrink-0" style={{ color: highlighted ? "var(--brand-053m)" : "var(--bb-text)" }}>
{price}
</p>
</div>
{description && (
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{description}</p>
)}
<button className="bb-btn bb-btn-sm bb-btn-outline self-start">
Записаться
</button>
</div>
);
}
/* ─── Карточка услуги ───────────────────────────────────────────────── */
function ServiceCard({
title, description, icon,
}: {
title: string; description: string; icon: string;
}) {
return (
<div
className="bb-service-card rounded-xl border p-5 flex flex-col gap-3 cursor-pointer transition-shadow"
style={{ borderColor: "var(--bb-border)", background: "#fff" }}
>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl"
style={{ background: "#dff0fa" }}
>
{icon}
</div>
<div>
<h3 className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}>{title}</h3>
<p className="text-xs leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>{description}</p>
</div>
<span className="text-xs font-medium" style={{ color: "var(--brand-053m)" }}>
Подробнее
</span>
</div>
);
}
/* ─── LLM текст ─────────────────────────────────────────────────────── */
const LLM_CARDS_TEXT = `КАРТОЧКИ — LLM-спецификация
Версия: v1.0 · /components/cards
КАРТОЧКА ВРАЧА (DoctorCard)
Структура: фото (110×160px) + имя + специализация + опыт + кнопка «Записаться»
Фото: 110px × 160px, object-fit: cover, border-radius: 8px, фон-placeholder: #dff0fa
Кнопка: .bb-btn-outline .bb-btn-sm
Источник: .doctor на perm.oclinica.ru
КАРТОЧКА НОВОСТИ (NewsCard)
Структура: превью (h=144px) + дата + заголовок + анонс (4 строки) + «Читать далее →»
Hover: background #eef4d1 + box-shadow 0 0 16px #9e9e9a
Источник: #block-views-last-news-block-1 .views-column на сайте (200×200px)
Без hover: background #fff, border 1px
КАРТОЧКА ОТЗЫВА (ReviewCard)
Структура: рейтинг (звёзды 1–5) + текст (4 строки, overflow hidden) + автор + дата + врач
Фон: #eef4d1 (светло-жёлтый) — из реального CSS сайта
Звёзды: SVG polygon, filled #f59e0b
КАРТОЧКА ЦЕНЫ (PriceCard)
Структура: услуга + цена (bold) + описание + кнопка
highlighted-вариант: border var(--brand-053m), bg #f0f9ff, цена тоже --brand-053m
КАРТОЧКА УСЛУГИ (ServiceCard)
Структура: иконка (emoji, 48×48px, bg #dff0fa) + заголовок + описание + «Подробнее →»
Hover: box-shadow (0.5rem smth)
БЕЙДЖИ (Badge)
Варианты: primary (#0089c3), success (#059669), warning (#d97706), danger (#dc2626), neutral (#6b7280)
Размер: text-xs, px-2.5 py-0.5, border-radius: full
CSS: inline-flex, font-weight: 600
ТЕГИ (Tag)
Варианты: default (border, text), filled (bg), removable (с кнопкой ×)
Цвет: --brand-053m или нейтральный (#e5e7eb bg)
Размер: text-xs, px-2 py-1, border-radius: 4px
АЛЕРТЫ (Alert)
Варианты: info (#dff0fa фон, #075985 текст), success (#d1fae5, #065f46), warning (#fef3c7, #92400e), error (#fee2e2, #991b1b)
Структура: иконка (16×16px) + заголовок + описание
Без кнопки закрыть в базовом варианте
ПРАВИЛА
✓ DoctorCard: всегда показывать фото-placeholder если нет фото (не ломать layout)
✓ ReviewCard: обрезать текст после 4 строк (WebkitLineClamp)
✓ PriceCard: highlighted = акционная или рекомендуемая позиция
✓ NewsCard: hover-эффект обязателен (#eef4d1 + box-shadow)
✓ Badge: не более 2–3 бейджей рядом
✓ Alert: одновременно не более 1 alert одного типа на экране
✕ Не смешивать типы карточек в одной сетке без заголовка секции
✕ Не использовать ReviewCard без рейтинга`.trim();
/* ─── Данные примеров ───────────────────────────────────────────────── */
const DOCTORS = [
{
name: "Иванова Анна Сергеевна",
specialty: "Оториноларинголог (ЛОР), высшая категория",
experience: "Стаж 18 лет",
},
{
name: "Петров Дмитрий Александрович",
specialty: "Сурдолог, аудиолог",
experience: "Стаж 12 лет",
},
{
name: "Соколова Мария Ивановна",
specialty: "Детский ЛОР, ринолог",
experience: "Стаж 9 лет",
},
];
const REVIEWS = [
{
author: "Елена К.",
date: "15 марта 2026",
rating: 5,
doctor: "Иванова А.С.",
text: "Очень довольна визитом. Доктор внимательно выслушала все жалобы, провела осмотр и объяснила причину заболевания. Назначила лечение, которое помогло уже через 3 дня. Рекомендую!",
},
{
author: "Михаил Р.",
date: "10 марта 2026",
rating: 4,
doctor: "Петров Д.А.",
text: "Хороший специалист, всё объяснил понятно. Подождал немного дольше, чем ожидал, но качество приёма на высоте. Буду обращаться снова.",
},
];
const PRICES = [
{ service: "Первичный приём ЛОР-врача", price: "1 500 ₽", description: "Включает осмотр и консультацию" },
{ service: "Повторный приём", price: "1 000 ₽", description: "До 14 дней после первичного" },
{ service: "Промывание миндалин", price: "800 ₽", highlighted: true, description: "Аппаратное — аккустический вакуум" },
{ service: "Аудиометрия", price: "1 200 ₽", description: "Исследование слуха" },
];
const SERVICES = [
{ title: "Лечение ангины и тонзиллита", description: "Консервативное и хирургическое лечение заболеваний миндалин", icon: "🦷" },
{ title: "Аудиология и сурдология", description: "Диагностика нарушений слуха, подбор слуховых аппаратов", icon: "👂" },
{ title: "Детский ЛОР", description: "Специализация на лечении ЛОР-заболеваний у детей от 0 лет", icon: "👶" },
{ title: "Ринология", description: "Лечение заболеваний носа и придаточных пазух", icon: "👃" },
];
/* ─── Коды примеров ─────────────────────────────────────────────────── */
const codeDoctorCard = `<!-- Карточка врача -->
<div class="doctor-card">
<img src="doctor.jpg" width="110" height="160" alt="ФИО" />
<div class="doctor-info">
<h3>Иванова Анна Сергеевна</h3>
<p class="specialty">Оториноларинголог, высшая категория</p>
<p class="experience">Стаж 18 лет</p>
<button class="bb-btn bb-btn-sm bb-btn-outline">Записаться</button>
</div>
</div>
/* CSS с сайта oclinica.ru */
.doctor .image { float:left; margin-right:20px; width:110px; height:160px; }
.doctor .item { float:left; width:170px; }
.doctor h3 { margin-top:0; height:32px; }`;
const codeNewsCard = `<!-- Карточка новости (сайт: 200×200px) -->
<div class="news-card">
<div class="news-preview">...</div>
<div class="news-body">
<time>15 марта 2026</time>
<h3>Заголовок новости</h3>
<p>Краткий анонс...</p>
<a href="#">Читать далее →</a>
</div>
</div>
/* CSS с сайта */
#block-views-last-news-block-1 .views-column {
background: #fff; width: 200px; height: 200px;
margin: 15px 8px; padding: 15px;
}
#block-views-last-news-block-1 .views-column:hover {
background: #eef4d1;
box-shadow: 0px 0px 16px 0px #9e9e9a;
}`;
const codeReviewCard = `<!-- Карточка отзыва -->
<div class="review-card">
<div class="stars">★★★★★</div>
<p class="text">Текст отзыва (4 строки, overflow: hidden)...</p>
<div class="author">
<span>Елена К.</span>
<time>15 марта 2026</time>
</div>
</div>
/* Стиль брендбука (фон из CSS сайта) */
.review-card { background: #eef4d1; border-radius: 12px; padding: 20px; }`;
const codePriceCard = `<!-- Карточка цены -->
<div class="price-card">
<div class="price-row">
<span class="service">Первичный приём ЛОР-врача</span>
<strong class="price">1 500 ₽</strong>
</div>
<p class="description">Включает осмотр и консультацию</p>
<button class="bb-btn bb-btn-sm bb-btn-outline">Записаться</button>
</div>`;
const codeBadges = `<!-- Бейджи -->
<span class="bb-badge bb-badge-primary">ЛОР</span>
<span class="bb-badge bb-badge-success">Принимает</span>
<span class="bb-badge bb-badge-warning">Ожидает</span>
<span class="bb-badge bb-badge-danger">Не принимает</span>
<span class="bb-badge bb-badge-neutral">Высшая категория</span>
<!-- Теги -->
<button class="bb-tag">Ухо</button>
<button class="bb-tag">Горло</button>
<button class="bb-tag bb-tag-active">Нос</button>
<!-- Алерт -->
<div class="bb-alert bb-alert-info">
<span class="bb-alert-icon"></span>
<div>
<strong>Запись открыта</strong>
<p>Вы можете записаться онлайн или по телефону.</p>
</div>
</div>`;
export default function CardsPage() {
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.3
</p>
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
Карточки
</h1>
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
Карточки врача, новости, отзыва, цены и услуги основные блоки контента сайта.
Бейджи, теги и алерты вспомогательные элементы.
</p>
</div>
{/* 1. Карточки врачей */}
<Section
id="doctor"
title="Карточка врача"
subtitle="Фото 110×160px, имя, специализация, стаж, кнопка записи. Соответствует .doctor на сайте."
>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{DOCTORS.map(d => <DoctorCard key={d.name} {...d} />)}
</div>
</Section>
{/* 2. Карточки новостей */}
<Section
id="news"
title="Карточка новости"
subtitle="Hover: bg #eef4d1 + box-shadow. Источник: #block-views-last-news-block-1 на сайте."
>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<NewsCard
date="15 марта 2026"
category="ЛОР"
title="Как правильно промывать нос при насморке"
snippet="Промывание носа — эффективный метод лечения и профилактики острого ринита. Рассказываем о правильной технике."
/>
<NewsCard
date="10 марта 2026"
category="Аудиология"
title="Новый слуховой аппарат в нашей клинике"
snippet="Мы начали подбор и настройку слуховых аппаратов нового поколения — незаметных и точных."
/>
<NewsCard
date="5 марта 2026"
title="Весенняя профилактика ЛОР-заболеваний"
snippet="Апрель — период обострений. Рекомендации нашего специалиста по укреплению иммунитета и защите."
/>
</div>
<p className="mt-3 text-xs" style={{ color: "var(--bb-text-muted)" }}>
* Наведите на карточку чтобы увидеть hover-эффект
</p>
</Section>
{/* 3. Карточки отзывов */}
<Section
id="review"
title="Карточка отзыва"
subtitle="Рейтинг (1–5 звёзд), текст 4 строки, автор, дата, врач. Фон #eef4d1 — с реального сайта."
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{REVIEWS.map(r => <ReviewCard key={r.author} {...r} />)}
</div>
</Section>
{/* 4. Карточки цен */}
<Section
id="price"
title="Карточка цены"
subtitle="Услуга + стоимость + описание + кнопка. Highlighted-вариант для акционных позиций."
>
<div className="flex flex-col gap-3">
{PRICES.map(p => <PriceCard key={p.service} {...p} />)}
</div>
</Section>
{/* 5. Карточки услуг */}
<Section
id="service"
title="Карточка услуги"
subtitle="Иконка + заголовок + описание + ссылка. Применяется в блоке «Наши услуги»."
>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{SERVICES.map(s => <ServiceCard key={s.title} {...s} />)}
</div>
</Section>
{/* 6. Бейджи и теги */}
<Section
id="badges"
title="Бейджи и теги"
subtitle="Статусные бейджи, теги-категории, алерты."
>
{/* Бейджи */}
<div className="mb-8">
<p className="text-sm font-medium mb-3" style={{ color: "var(--bb-text)" }}>Бейджи (статус)</p>
<div className="flex flex-wrap gap-2">
{[
{ label: "Основной", bg: "var(--brand-053m)", color: "#fff" },
{ label: "Принимает", bg: "#059669", color: "#fff" },
{ label: "Высшая категория", bg: "#d97706", color: "#fff" },
{ label: "Не принимает", bg: "#dc2626", color: "#fff" },
{ label: "Нейтральный", bg: "#6b7280", color: "#fff" },
{ label: "Новинка", bg: "#dff0fa", color: "var(--brand-053m)" },
].map(b => (
<span
key={b.label}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold"
style={{ background: b.bg, color: b.color }}
>
{b.label}
</span>
))}
</div>
</div>
{/* Теги */}
<div className="mb-8">
<p className="text-sm font-medium mb-3" style={{ color: "var(--bb-text)" }}>Теги (категории)</p>
<div className="flex flex-wrap gap-2">
{["Ухо", "Горло", "Нос", "Аудиология", "Детский ЛОР", "Хирургия"].map((tag, i) => (
<button
key={tag}
className="inline-flex items-center px-3 py-1 rounded text-xs font-medium border transition-colors"
style={
i === 0
? { background: "var(--brand-053m)", color: "#fff", borderColor: "var(--brand-053m)" }
: { background: "#fff", color: "var(--bb-text)", borderColor: "var(--bb-border)" }
}
>
{tag}
</button>
))}
</div>
</div>
{/* Алерты */}
<div>
<p className="text-sm font-medium mb-3" style={{ color: "var(--bb-text)" }}>Алерты</p>
<div className="flex flex-col gap-3">
{[
{ type: "info", bg: "#dff0fa", color: "#075985", icon: "", title: "Информация", text: "Запись открыта. Вы можете записаться онлайн или по телефону." },
{ type: "success", bg: "#d1fae5", color: "#065f46", icon: "✓", title: "Успешно", text: "Ваша запись подтверждена. Ждём вас 20 марта в 10:00." },
{ type: "warning", bg: "#fef3c7", color: "#92400e", icon: "⚠", title: "Внимание", text: "Не забудьте взять паспорт и полис ОМС на приём." },
{ type: "error", bg: "#fee2e2", color: "#991b1b", icon: "✕", title: "Ошибка", text: "К сожалению, это время уже занято. Выберите другое." },
].map(a => (
<div
key={a.type}
className="flex gap-3 p-4 rounded-xl border"
style={{ background: a.bg, borderColor: a.bg, color: a.color }}
>
<span className="text-base font-bold shrink-0 mt-0.5">{a.icon}</span>
<div>
<p className="text-sm font-semibold mb-0.5">{a.title}</p>
<p className="text-sm opacity-90">{a.text}</p>
</div>
</div>
))}
</div>
</div>
</Section>
{/* 7. Код */}
<Section
id="code"
title="Примеры кода"
subtitle="HTML-структура и CSS-справка."
>
<div className="space-y-4">
<CodeCopy lang="HTML — DoctorCard + CSS с сайта" code={codeDoctorCard} />
<CodeCopy lang="HTML — NewsCard + CSS с сайта" code={codeNewsCard} />
<CodeCopy lang="HTML — ReviewCard" code={codeReviewCard} />
<CodeCopy lang="HTML — PriceCard" code={codePriceCard} />
<CodeCopy lang="HTML — Badges, Tags, Alerts" code={codeBadges} />
</div>
</Section>
{/* LLM-блок */}
<LlmBlock path="/components/cards" version="v1.0" specText={LLM_CARDS_TEXT}>
<LlmSection title="Типы карточек" />
<LlmTable
headers={["Карточка", "Ключевые размеры", "Источник на сайте", "Фон / hover"]}
rows={[
["DoctorCard", "фото 110×160px, layout: flex", ".doctor .image + .doctor .item", "#fff / —"],
["NewsCard", "preview h=144px, grid 3 col", "#block-views-last-news-block-1 .views-column", "#fff / #eef4d1 + shadow"],
["ReviewCard", "4 строки текста, рейтинг", ".node-reviews", "#eef4d1 / —"],
["PriceCard", "flex row: name + price", ".field-name-field-price-priem", "#fff / highlighted: #f0f9ff"],
["ServiceCard", "иконка 48×48, grid 4 col", "—", "#fff / shadow"],
]}
/>
<LlmSection title="Бейджи, теги, алерты" />
<LlmTable
headers={["Элемент", "Варианты", "Размер", "Применение"]}
rows={[
["Badge", "primary / success / warning / danger / neutral", "text-xs, px-2.5, rounded-full", "Статус врача, категория, акция"],
["Tag", "default / active", "text-xs, px-3, rounded-4px", "Фильтры, категории услуг"],
["Alert", "info / success / warning / error", "p-4, border-radius 12px", "Системные сообщения пользователю"],
]}
/>
<LlmSection title="Правила применения" />
<LlmRules
rules={[
{ ok: true, text: "DoctorCard: всегда фото-placeholder если нет фото" },
{ ok: true, text: "NewsCard: hover #eef4d1 + box-shadow (из реального CSS сайта)" },
{ ok: true, text: "ReviewCard: обрезать текст после 4 строк (WebkitLineClamp: 4)" },
{ ok: true, text: "PriceCard highlighted = акционная / рекомендуемая позиция" },
{ ok: true, text: "Alert: один тип одновременно на экране" },
{ ok: false, text: "Не смешивать типы карточек в одной сетке без заголовка" },
{ ok: false, text: "Не использовать ReviewCard без рейтинга" },
{ ok: false, text: "Не ставить более 3 бейджей рядом" },
]}
/>
</LlmBlock>
</div>
);
}
+723
View File
@@ -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>
);
}
+417 -18
View File
@@ -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";
/* ─── Утилиты конвертации ──────────────────────────────────────────── */
@@ -54,42 +55,42 @@ const BRAND_COLORS = [
{
oracal: "053M",
name: "Основной бирюзовый",
hex: "#7ecfca",
hex: "#0089c3",
usage: "Акцентный цвет, CTA-кнопки, иконки, активные состояния",
cssVar: "--brand-053m",
},
{
oracal: "073M",
name: "Тёмный серо-голубой",
hex: "#5b7b87",
hex: "#53514e",
usage: "Тёмный фон, хедер, акценты на тёмных поверхностях",
cssVar: "--brand-073m",
},
{
oracal: "066M",
name: "Средний бирюзовый",
hex: "#5bb5ad",
hex: "#00818c",
usage: "Вторичные акценты, фоны секций, иллюстрации",
cssVar: "--brand-066m",
},
{
oracal: "050M",
name: "Тёмно-синий",
hex: "#1b4c72",
hex: "#1b2e5d",
usage: "Наружная реклама, полиграфия, заголовки на светлом фоне",
cssVar: "--brand-050m",
},
{
oracal: "081M",
name: "Бежевый",
hex: "#c4a882",
hex: "#a8885c",
usage: "Форма сотрудников, оффлайн носители, тёплые акценты",
cssVar: "--brand-081m",
},
{
oracal: "080M",
name: "Тёмно-коричневый",
hex: "#5c2e0e",
hex: "#432f1e",
usage: "Текст на бежевых поверхностях, логотип на форме",
cssVar: "--brand-080m",
},
@@ -102,14 +103,95 @@ const BRAND_COLORS = [
},
];
/* ─── Соответствие цветов ──────────────────────────────────────────── */
const COLOR_MAPPING = [
{
brand: { oracal: "053M", name: "Основной бирюзовый", hex: "#0089c3" },
web: { name: "Бирюзовый", hex: "#63bac3", count: 4 },
note: "Oracal — насыщенный синий; сайт — светлее и голубее",
},
{
brand: { oracal: "073M", name: "Тёмный серо-голубой", hex: "#53514e" },
web: { name: "Серо-бирюзовый", hex: "#60959c", count: 7 },
note: "Oracal — тёмно-серый; сайт — серо-бирюзовый (значительное расхождение)",
},
{
brand: { oracal: "066M", name: "Средний бирюзовый", hex: "#00818c" },
web: { name: "Бирюзовый средний", hex: "#52b4bd", count: 4 },
note: "Oracal — тёмный бирюзовый; сайт — светлее",
},
{
brand: { oracal: "081M", name: "Бежевый", hex: "#a8885c" },
web: { name: "Бежевый", hex: "#bf9975", count: 12 },
note: "Oracal — тёплый охристый; сайт — прохладнее и светлее",
},
{
brand: { oracal: "050M", name: "Тёмно-синий", hex: "#1b2e5d" },
web: null,
note: "Не найден в CSS сайта",
},
{
brand: { oracal: "080M", name: "Тёмно-коричневый", hex: "#432f1e" },
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: "Белый на тёмно-синем" },
{ fg: "#ffffff", bg: "#5c2e0e", label: "Белый на тёмно-коричневом" },
{ fg: "#ffffff", bg: "#5bb5ad", label: "Белый на среднем бирюзовом" },
{ fg: "#111827", bg: "#7ecfca", label: "Тёмный текст на основном бирюзовом" },
{ fg: "#111827", bg: "#c4a882", label: "Тёмный текст на бежевом" },
{ fg: "#5c2e0e", bg: "#c4a882", label: "Тёмно-коричневый на бежевом (форма)" },
{ fg: "#ffffff", bg: "#53514e", label: "Белый на тёмном серо-голубом (073M)" },
{ fg: "#ffffff", bg: "#1b2e5d", label: "Белый на тёмно-синем (050M)" },
{ fg: "#ffffff", bg: "#432f1e", label: "Белый на тёмно-коричневом (080M)" },
{ fg: "#ffffff", bg: "#00818c", label: "Белый на среднем бирюзовом (066M)" },
{ fg: "#111827", bg: "#0089c3", label: "Тёмный текст на основном бирюзовом (053M)" },
{ fg: "#111827", bg: "#a8885c", label: "Тёмный текст на бежевом (081M)" },
{ fg: "#432f1e", bg: "#a8885c", label: "Тёмно-коричневый на бежевом (форма, 080M/081M)" },
];
/* ─── Компоненты ───────────────────────────────────────────────────── */
@@ -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.2 · 2026-03-22
ФИРМЕННЫЕ ЦВЕТА (Oracal) — точные значения из каталога
Oracal | Название | HEX | RGB | CSS-переменная | Применение
053M | Основной бирюзовый | #0089C3 | rgb(0,137,195) | --brand-053m | Акцент, CTA-кнопки, иконки, активные состояния
073M | Тёмный серо-голубой | #53514E | rgb(83,81,78) | --brand-073m | Тёмный фон, хедер, заголовки навигации
066M | Средний бирюзовый | #00818C | rgb(0,129,140) | --brand-066m | Вторичные акценты, фоны секций
050M | Тёмно-синий | #1B2E5D | rgb(27,46,93) | --brand-050m | Наружная реклама, полиграфия, заголовки
081M | Бежевый | #A8885C | rgb(168,136,92) | --brand-081m | Форма сотрудников, тёплые акценты
080M | Тёмно-коричневый | #432F1E | rgb(67,47,30) | --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 #0089C3 → #63BAC3 (Oracal ярко-синий; сайт светлее и голубее)
073M #53514E → #60959C (Oracal тёмно-серый; сайт серо-бирюзовый — значительное расхождение)
066M #00818C → #52B4BD (Oracal тёмный бирюзовый; сайт светлее)
081M #A8885C → #BF9975 (Oracal тёплый охристый; сайт прохладнее и светлее)
050M #1B2E5D → не найден в CSS сайта
080M #432F1E → не найден в CSS сайта
КОНТРАСТНОСТЬ WCAG 2.1
#FFFFFF / #53514E | 7.9:1 | AA PASS | AAA PASS
#FFFFFF / #1B2E5D | 13.2:1 | AA PASS | AAA PASS
#FFFFFF / #432F1E | 12.6:1 | AA PASS | AAA PASS
#FFFFFF / #00818C | 4.7:1 | AA PASS | AAA FAIL
#111827 / #0089C3 | 4.7:1 | AA PASS | AAA FAIL
#111827 / #A8885C | 5.5:1 | AA PASS | AAA FAIL
#432F1E / #A8885C | 3.8:1 | AA FAIL | AAA FAIL | только крупный текст ≥18pt
ПРАВИЛА
✓ Только цвета из фирменной палитры
✓ Digital → цвета сайта; оффлайн → коды Oracal
✓ Текст на цветном фоне: минимум WCAG AA (4.5:1)
✓ Белый текст на: 073M (#53514E), 066M (#00818C), 050M (#1B2E5D), 080M (#432F1E)
✓ Тёмный текст (#111827) на: 053M (#0089C3), 081M (#A8885C)
✕ Произвольные цвета вне фирменной палитры
✕ Изменение насыщенности / оттенка фирменных цветов
✕ Тёплые и холодные акценты рядом без нейтрального разделителя`.trim();
/* ─── Страница ─────────────────────────────────────────────────────── */
export default function ColorsPage() {
return (
@@ -292,12 +480,12 @@ export default function ColorsPage() {
<div className="mt-4 flex items-center justify-between">
<div
className="px-4 py-3 rounded-lg border text-sm flex items-center gap-2"
style={{ borderColor: "#fde68a", background: "#fffbeb", color: "#92400e" }}
style={{ borderColor: "#bae6fd", background: "#f0f9ff", color: "#075985" }}
>
<span></span>
<span></span>
<span>
HEX-значения приблизительны. Для оффлайн-носителей используйте
официальные коды Oracal.
HEX-значения рассчитаны по точным RGB из каталога Oracal.
Для оффлайн-носителей используйте официальные коды Oracal.
</span>
</div>
<button
@@ -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.2"
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 (#53514E), 066M (#00818C), 050M (#1B2E5D), 080M (#432F1E)" },
{ ok: true, text: "Тёмный текст (#111827) на: 053M (#0089C3), 081M (#A8885C)" },
{ ok: false, text: "Произвольные цвета вне фирменной палитры" },
{ ok: false, text: "Изменение насыщенности / оттенка фирменных цветов" },
{ ok: false, text: "Тёплые + холодные акценты рядом без разделителя" },
]} />
</div>
</LlmBlock>
</section>
</div>
);
}
+89 -1
View File
@@ -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>
);
}
+160 -1
View File
@@ -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>
);
}
+155 -8
View File
@@ -3,13 +3,13 @@
/* ─── Бренд-токены О!Клиника ─────────────────────────────────────────── */
/* Цвета уточняются в Sprint 2 по таблице Oracal */
:root {
/* Фирменные цвета (приблизительноуточнить в Sprint 2) */
--brand-053m: #7ecfca; /* Основной бирюзовый (Oracal 053M) */
--brand-073m: #5b7b87; /* Тёмный серо-голубой (Oracal 073M) */
--brand-066m: #5bb5ad; /* Средний бирюзовый (Oracal 066M) */
--brand-050m: #1b4c72; /* Тёмно-синий, наружная реклама (Oracal 050M) */
--brand-081m: #c4a882; /* Бежевый (Oracal 081M) */
--brand-080m: #5c2e0e; /* Тёмно-коричневый (Oracal 080M) */
/* Фирменные цвета — точные RGB из каталога Oracal */
--brand-053m: #0089c3; /* Основной бирюзовый (Oracal 053M) · rgb(0,137,195) */
--brand-073m: #53514e; /* Тёмный серо-голубой (Oracal 073M) · rgb(83,81,78) */
--brand-066m: #00818c; /* Средний бирюзовый (Oracal 066M) · rgb(0,129,140) */
--brand-050m: #1b2e5d; /* Тёмно-синий, наружная реклама (Oracal 050M) · rgb(27,46,93) */
--brand-081m: #a8885c; /* Бежевый (Oracal 081M) · rgb(168,136,92) */
--brand-080m: #432f1e; /* Тёмно-коричневый (Oracal 080M) · rgb(67,47,30) */
--brand-white: #ffffff;
/* UI-цвета брендбука */
@@ -17,7 +17,7 @@
--bb-sidebar-border: #e5e7eb;
--bb-sidebar-text: #374151;
--bb-sidebar-text-muted: #6b7280;
--bb-sidebar-active-bg: #e0f5f4;
--bb-sidebar-active-bg: #dff0fa;
--bb-sidebar-active-text: var(--brand-053m);
--bb-sidebar-section: #9ca3af;
--bb-content-bg: #ffffff;
@@ -42,3 +42,150 @@ 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(0, 137, 195, 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 4) ────────────────────────────────────── */
.bb-news-card:hover {
background: #eef4d1 !important;
box-shadow: 0 0 16px 0 #9e9e9a;
}
.bb-service-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
/* ─── Тумблер (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;
}
+116 -125
View File
@@ -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)" }}>
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
import Image from "next/image";
export const metadata: Metadata = {
title: "Печатные материалы | Брендбук О!Клиника",
title: "Печатные материалы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
};
function Section({
+14 -98
View File
@@ -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>
{/* Цветовая схема */}
+6 -7
View File
@@ -27,9 +27,9 @@ const NAV: NavSection[] = [
{
title: "Компоненты",
items: [
{ label: "Кнопки", href: "/components/buttons", soon: true },
{ label: "Форм-контролы", href: "/components/forms", soon: true },
{ label: "Карточки", href: "/components/cards", soon: true },
{ label: "Кнопки", href: "/components/buttons" },
{ label: "Форм-контролы", href: "/components/forms" },
{ label: "Карточки", href: "/components/cards" },
{ label: "Бейджи и теги", href: "/components/badges", soon: true },
{ label: "Алерты", href: "/components/alerts", soon: true },
{ label: "Модальные окна", href: "/components/modals", soon: true },
@@ -40,8 +40,8 @@ const NAV: NavSection[] = [
{
title: "Блоки",
items: [
{ label: "Hero", href: "/blocks/hero", soon: true },
{ label: "CEO-текст", href: "/blocks/ceo", soon: true },
{ label: "Hero", href: "/blocks/hero" },
{ label: "CEO-текст", href: "/blocks/ceo" },
{ label: "Наши врачи", href: "/blocks/doctors", soon: true },
{ label: "Отзывы", href: "/blocks/reviews", soon: true },
{ label: "Новости", href: "/blocks/news", 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 5 · v0.5.0
</div>
</aside>
);
+175
View File
@@ -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>
);
}
+45
View File
@@ -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>
);
}
);
+60
View File
@@ -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>
);
}
+59
View File
@@ -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>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

+452
View File
@@ -0,0 +1,452 @@
# LLM Context — Цифровой брендбук Клиники
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
**Версия контекста:** 4.1
**Дата обновления:** 2026-03-22
**Актуальный спринт:** Sprint 5
**Сайт клиники:** 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-значения рассчитаны по **точным RGB
из каталога Oracal**. Для печати и оффлайн-носителей используй коды Oracal, не HEX.
| Oracal | Название | HEX | RGB | HSL | CSS-переменная | Применение |
|--------|-----------------------|-----------|-------------------|-------------------|------------------|------------|
| 053M | Основной бирюзовый | `#0089c3` | rgb(0,137,195) | hsl(198,100%,38%) | `--brand-053m` | Акцент, CTA-кнопки, иконки, активные состояния |
| 073M | Тёмный серо-голубой | `#53514e` | rgb(83,81,78) | hsl(38,3%,32%) | `--brand-073m` | Тёмный фон, хедер, заголовки навигации |
| 066M | Средний бирюзовый | `#00818c` | rgb(0,129,140) | hsl(185,100%,27%) | `--brand-066m` | Вторичные акценты, фоны секций, иллюстрации |
| 050M | Тёмно-синий | `#1b2e5d` | rgb(27,46,93) | hsl(223,55%,24%) | `--brand-050m` | Наружная реклама, полиграфия, заголовки на светлом |
| 081M | Бежевый | `#a8885c` | rgb(168,136,92) | hsl(35,30%,51%) | `--brand-081m` | Форма сотрудников, оффлайн носители, тёплые акценты |
| 080M | Тёмно-коричневый | `#432f1e` | rgb(67,47,30) | hsl(27,38%,19%) | `--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 Основной бирюзовый | `#0089c3` | Бирюзовый | `#63bac3` | Oracal ярко-синий; сайт светлее и голубее |
| 073M Тёмный серо-голубой | `#53514e` | Серо-бирюзовый | `#60959c` | Oracal тёмно-серый; сайт серо-бирюзовый (значительное расхождение) |
| 066M Средний бирюзовый | `#00818c` | Бирюзовый средний | `#52b4bd` | Oracal тёмный бирюзовый; сайт светлее |
| 081M Бежевый | `#a8885c` | Бежевый | `#bf9975` | Oracal тёплый охристый; сайт прохладнее и светлее |
| 050M Тёмно-синий | `#1b2e5d` | — | — | Не найден в CSS сайта |
| 080M Тёмно-коричневый | `#432f1e` | — | — | Не найден в CSS сайта |
**Важно:** расхождения ожидаемы — это цифровая адаптация плёночных цветов под экран.
При разработке digital-материалов используй цвета сайта (раздел 2.2), не Oracal.
### 2.4 Контрастность пар (WCAG 2.1)
| Пара | Коэффициент | AA (4.5:1) | AAA (7:1) | AA large (3:1) |
|----------------------------------------------|-------------|------------|-----------|----------------|
| Белый на тёмном серо-голубом (#53514e 073M) | 7.9:1 | ✓ | ✓ | ✓ |
| Белый на тёмно-синем (#1b2e5d 050M) | 13.2:1 | ✓ | ✓ | ✓ |
| Белый на тёмно-коричневом (#432f1e 080M) | 12.6:1 | ✓ | ✓ | ✓ |
| Белый на среднем бирюзовом (#00818c 066M) | 4.7:1 | ✓ | ✕ | ✓ |
| Тёмный (#111827) на основном бирюзовом (#0089c3 053M) | 4.7:1 | ✓ | ✕ | ✓ |
| Тёмный (#111827) на бежевом (#a8885c 081M) | 5.5:1 | ✓ | ✕ | ✓ |
| Тёмно-коричневый (#432f1e) на бежевом (#a8885c) | 3.8:1 | ✕ | ✕ | ✓ (только крупный ≥18pt) |
---
## 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 Цветовые варианты
| Вариант | Фон | Логотип | Применение |
|---------|-----|---------|------------|
| Основной | Белый / светлый | Полноцветный | Сайт, полиграфия на белом |
| Инвертированный | Тёмный (#53514e, #1b2e5d) | Белый (`filter: brightness(0) invert(1)`) | Хедер, тёмные секции |
| На форме (беж) | Бежевый (#a8885c / Oracal 081M) | Коричневый (#432f1e / Oracal 080M) | Одежда персонала |
| На форме (синий) | Тёмно-синий (#1b2e5d / 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
/* Цвета бренда (точные RGB из каталога Oracal) */
--brand-053m: #0089c3; /* Основной бирюзовый · rgb(0,137,195) */
--brand-073m: #53514e; /* Тёмный серо-голубой · rgb(83,81,78) */
--brand-066m: #00818c; /* Средний бирюзовый · rgb(0,129,140) */
--brand-050m: #1b2e5d; /* Тёмно-синий · rgb(27,46,93) */
--brand-081m: #a8885c; /* Бежевый · rgb(168,136,92) */
--brand-080m: #432f1e; /* Тёмно-коричневый · rgb(67,47,30) */
--brand-white: #ffffff; /* Белый */
/* UI брендбука */
--bb-sidebar-bg: #f8f9fa;
--bb-sidebar-border: #e5e7eb;
--bb-sidebar-text: #374151;
--bb-sidebar-text-muted: #6b7280;
--bb-sidebar-active-bg: #dff0fa; /* светло-синий под 053M */
--bb-sidebar-active-text: var(--brand-053m);
--bb-sidebar-section: #9ca3af;
--bb-content-bg: #ffffff;
--bb-border: #e5e7eb;
--bb-text: #111827;
--bb-text-muted: #6b7280;
```
---
## 6. Оффлайн носители
### 6.1 Форма сотрудников
**Варианты:**
- Бежевый: ткань цвета Oracal 081M (#a8885c), логотип Oracal 080M (#432f1e, коричневый), расположение — левая сторона груди
- Синий: ткань цвета Oracal 050M (#1b2e5d, тёмно-синий), логотип белый (Oracal 010), расположение — левая сторона груди
**Размеры логотипа на форме:**
- Размеры до 46: 70 × 25,5 мм
- Размеры от 48: 90 × 32,8 мм
### 6.2 Бейджи
**Размер:** 70 × 30 мм
**Варианты:** серый (светлый) и белый
**Состав текста:** имя, должность, учёная степень (при наличии)
**Шрифт:** DINPro
**Логотип:** левый верхний угол или левая часть
### 6.3 Внутренняя навигация
**Материал:** оргстекло
**Плёнки:** Oracal 053M (#0089c3) и 073M (#53514e)
**Типы табличек:**
- Таблички на дверях кабинетов: номер кабинета, профиль врача с фото и QR-кодом
- Указатели по этажам: стрелки направлений + номера кабинетов
**Нумерация кабинетов:**
- Двузначное число, крупный шрифт (DINPro Bold)
- Фон: бирюзовый (053M), номер: белый
- Пример реализации: кабинеты 13, 31, «Кабинет 04»
### 6.4 Брендирование транспорта (трамвай)
**Зоны оклейки:**
- Борта: Oracal 053M (#0089c3) + 073M (#53514e)
- Передняя часть: Oracal 066M (#00818c) + 050M (#1b2e5d)
- Акценты: Oracal 081M (#a8885c) + 080M (#432f1e)
**Все 6 фирменных цветов присутствуют на транспорте.**
---
## 7. Структура брендбука (страницы)
| URL | Статус | Описание |
|-----|--------|----------|
| `/foundation/logo` | ✅ Готова | Логотип, варианты, охранная зона, правила |
| `/foundation/colors` | ✅ Готова | Палитра (Oracal точные RGB), контраст WCAG, цвета сайта, соответствие |
| `/foundation/typography` | ✅ Готова | DINPro + Fira Sans, шкала стилей |
| `/foundation/icons` | 🔜 Скоро | Иконография |
| `/offline/uniform` | ✅ Готова | Форма сотрудников |
| `/offline/badges` | ✅ Готова | Бейджи |
| `/offline/navigation` | ✅ Готова | Внутренняя навигация |
| `/offline/transport` | ✅ Готова | Брендирование транспорта |
| `/components/buttons` | ✅ Готова | Кнопки — 4 варианта с реального сайта, размеры, состояния |
| `/components/forms` | ✅ Готова | Форм-контролы — Input/Textarea/Select/Checkbox/Radio/Toggle |
| `/components/cards` | 🚧 Sprint 4 | Карточки — врач, услуга, новость, отзыв, цена + бейджи/теги/алерты |
| `/components/*` | 🔜 Sprint 4–5 | Модалки, таблицы, навигация |
| `/blocks/*` | 🔜 Sprint 58 | Hero, врачи, отзывы, новости, формы |
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты |
---
## 8. Правила применения цветов
### Разрешено
- Использовать только цвета из фирменной палитры (раздел 2.1)
- Для digital: адаптировать к цветам сайта (раздел 2.2)
- Текст на цветном фоне — минимум WCAG AA (4.5:1)
- Белый текст на тёмных фонах: 073M (#53514e), 066M (#00818c), 050M (#1b2e5d), 080M (#432f1e)
- Тёмный текст (#111827) на светлых: 053M (#0089c3), 081M (#a8885c)
- Коричневый логотип (080M) на бежевом фоне (081M) — только крупный текст (3.8:1)
### Запрещено
- Использовать произвольные цвета вне палитры
- Осветлять, затемнять или менять оттенок фирменных цветов
- Текст с контрастом ниже 3:1 (даже для крупного текста)
- Размещать тёплые и холодные акценты рядом без разделителя
### Иерархия цветов
```
Основной бирюзовый (053M / #0089c3) ← главный идентификатор бренда
└── Средний бирюзовый (066M / #00818c) ← вторичный акцент
└── Тёмный серо-голубой (073M / #53514e) ← фоны, хедер
Тёмно-синий (050M / #1b2e5d) ← авторитет, полиграфия
Бежевый (081M / #a8885c) ← тепло, форма
└── Тёмно-коричневый (080M / #432f1e) ← текст на бежевом
```
---
## 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`
Варианты скопированы с реального сайта perm.oclinica.ru.
CSS-классы в `globals.css`. Компонент: `@/components/ui/Button` (React, "use client").
| Вариант | CSS класс | Фон | Текст | Граница | Border-radius | Применение |
|---------|------------------|----------|---------|-----------|---------------|------------|
| primary | .bb-btn-primary | #FFA39C | #fff | #FF847B | 7px, bold, shadow | Форма записи «Запишите меня!» |
| outline | .bb-btn-outline | #fff | #BF9975 | #BF9975 | 7px | «Записаться на приём» |
| teal | .bb-btn-teal | #60959C | #fff | прозрачная| 7px | «Позвонить» |
| pill | .bb-btn-pill | #E9E4D4 | #333 | #D5CFBD | 25px | «Заказать звонок» |
| Размер | CSS класс | padding | font-size |
|--------|------------|--------------|-----------|
| sm | .bb-btn-sm | 4px 11px | 13px |
| md | .bb-btn-md | 8px 16px | 14px |
| lg | .bb-btn-lg | 10px 24px | 18px |
**Состояния:** default · hover (brightness 0.9) · active (brightness 0.82) · loading (spinner) · disabled (opacity 0.5)
### Форм-контролы (Forms) · `/components/forms`
Размеры соответствуют entityform-блокам на реальном сайте.
| Элемент | CSS класс | Тег HTML | Высота | Описание |
|----------|------------------|----------------------------|---------|----------|
| Input | .bb-input | `<input>` | 50px | text, email, password · border 1px #ccc · radius 4px |
| Textarea | .bb-textarea | `<textarea>` | ≥100px | многострочный, resize:vertical |
| Select | .bb-select | `<select>` | 50px | с кастомной SVG-стрелкой |
| Checkbox | .bb-checkbox | `<input type="checkbox">` | 16×16px | accent-color: #53514e |
| Radio | .bb-radio | `<input type="radio">` | 16×16px | accent-color: #53514e |
| Toggle | .bb-toggle-track | React-компонент `<Toggle>` | 24px | 44×24px track + 20px thumb |
**Состояния полей:** default (border 1px solid #ccc) · focus (border #0089c3 + box-shadow rgba(0,137,195,0.2)) · error (.bb-error, border #dc2626) · disabled (opacity 0.5)
**Контекст на сайте:** фон формы #b8e6ed, ширина полей 302px, entityform-блоки.
**Toggle:** выкл → track #d1d5db · вкл → track #53514e · thumb: белый круг 20×20px.
---
## 9b. Карточки, бейджи и алерты (Sprint 4)
### Карточки · `/components/cards`
| Карточка | Размеры ключевые | Источник на сайте | Фон / hover |
|-------------|-------------------------|------------------------------------------------|--------------------------|
| DoctorCard | фото 110×160px | `.doctor .image` + `.doctor .item` | #fff / — |
| NewsCard | preview h=144px | `#block-views-last-news-block-1 .views-column` | #fff / **#eef4d1** + shadow |
| ReviewCard | 4-строчный clamp | `.node-reviews` | **#eef4d1** / — |
| PriceCard | flex row: name + price | `.field-name-field-price-priem` | #fff / highlighted: #f0f9ff |
| ServiceCard | иконка 48×48px | — (нет прямого аналога) | #fff / shadow |
**DoctorCard:** фото 110×160px (placeholder фон `#dff0fa`), кнопка `.bb-btn-outline .bb-btn-sm`
**NewsCard hover CSS:** `background: #eef4d1; box-shadow: 0 0 16px 0 #9e9e9a;` — взят с реального сайта
**ReviewCard:** звёзды SVG, заливка `#f59e0b`, `WebkitLineClamp: 4`
### Бейджи, теги, алерты
| Элемент | Варианты | Применение |
|---------|----------|------------|
| Badge | primary (#0089c3) / success (#059669) / warning (#d97706) / danger (#dc2626) / neutral (#6b7280) | Статус врача, категория, акция |
| Tag | default (border) / active (brand bg) | Фильтры, категории услуг |
| Alert | info (#dff0fa/#075985) / success (#d1fae5/#065f46) / warning (#fef3c7/#92400e) / error (#fee2e2/#991b1b) | Системные сообщения |
**CSS класс hover:** `.bb-news-card:hover` в globals.css
---
## 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-блок на логотипе |
| 4.0 | 2026-03-22 | Sprint 4 start: исправлены цвета Oracal (точные RGB из каталога), кнопки/формы по реальному сайту |
| 4.1 | 2026-03-22 | Sprint 4 done: карточки (DoctorCard/NewsCard/ReviewCard/PriceCard/ServiceCard), бейджи/теги/алерты |
---
## 12. Что обновлять в этом файле
При каждом спринте добавляй:
- Новые компоненты и их спецификации (цвета, размеры, состояния)
- Новые правила применения, выявленные при разработке
- Изменения в палитре или типографике
- Новые паттерны и примеры кода
**Соответствующий раздел в TZ.md:** ФТ-03-LLM
**Соответствующий раздел в SPRINTS.md:** задача «LLM-блок» в каждом спринте
+73 -30
View File
@@ -81,58 +81,93 @@
### Фактические результаты
- Страница `/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)
**Результат спринта:** Раздел «Базовые компоненты» полностью готов. Стили соответствуют реальному сайту.
---
## Sprint 4 — Карточки (Cards)
## Sprint 4 — Карточки (Cards) ✅ ЗАВЕРШЁН
**Цель:** Все типы карточек, используемых на сайте.
### Задачи
- [ ] FE: Карточка врача (DoctorCard) — фото, имя, специализация, кнопка записи
- [ ] FE: Карточка услуги / заболевания
- [ ] FE: Карточка новости — превью, дата, заголовок, анонс, читать далее
- [ ] FE: Карточка отзыва — автор, текст, рейтинг (звёзды), дата
- [ ] FE: Карточка цены — услуга, стоимость, описание
- [ ] FE: Бейджи, теги, алерты (inline и toast)
- [ ] FE: Страница «Компоненты → Карточки» с документацией
- [x] Docs: Обновить `docs/LLM_CONTEXT.md` → версия 4.0
- [x] FE: Добавить LLM-блок на страницу «Карточки» — v1.0
- [x] FE: Карточка врача (DoctorCard) — фото 110×160px, имя, специализация, стаж, кнопка outline
- [x] FE: Карточка услуги / заболевания (ServiceCard) — иконка + заголовок + описание
- [x] FE: Карточка новости — превью, дата, заголовок, анонс, hover #eef4d1 (CSS сайта)
- [x] FE: Карточка отзыва — рейтинг SVG-звёзды, 4-строчный clamp, фон #eef4d1
- [x] FE: Карточка цены — услуга + стоимость + highlighted-вариант
- [x] FE: Бейджи (6 вариантов), теги (default/active), алерты (info/success/warning/error)
- [x] FE: Страница `/components/cards` с документацией и LLM-блоком
**Результат спринта:** Все карточки задокументированы и показаны в брендбуке.
### Фактические результаты
- **5 типов карточек:** DoctorCard, NewsCard, ReviewCard, PriceCard, ServiceCard
- **NewsCard hover** — bg `#eef4d1` + box-shadow `0 0 16px #9e9e9a` (1:1 с реальным CSS сайта)
- **ReviewCard** — фон `#eef4d1` из CSS сайта, WebkitLineClamp: 4
- **Бейджи** — 6 вариантов (primary/success/warning/danger/neutral/outline-blue)
- **Алерты** — info (#dff0fa), success (#d1fae5), warning (#fef3c7), error (#fee2e2)
- **Исправлены цвета Oracal** — точные RGB из каталога для всех 6 плёнок
- **Деплой:** https://web-oclinica.vercel.app (production)
**Результат спринта:** Все карточки задокументированы. Цвета Oracal исправлены по каталогу.
---
@@ -141,6 +176,8 @@
**Цель:** Ключевые верхние блоки страниц.
### Задачи
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации Hero и CEO-блоков
- [ ] FE: Добавить LLM-блоки на страницы Hero и CEO-текст
- [ ] FE: Hero-блок вариант 1 — фон + заголовок + CTA
- [ ] FE: Hero-блок вариант 2 — иллюстрация сбоку
- [ ] FE: Hero-блок вариант 3 — с встроенной формой записи
@@ -158,6 +195,8 @@
**Цель:** Блоки и компоненты, связанные с врачами.
### Задачи
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации блоков врачей
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Врачи»
- [ ] FE: Блок «Наши врачи» — сетка карточек с фильтром по специализации
- [ ] FE: Блок «Врач — профиль» (полная страница): фото, биография, специализации, расписание
- [ ] FE: Компонент «Расписание / слоты записи»
@@ -174,6 +213,8 @@
**Цель:** Контентные блоки сайта.
### Задачи
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации блоков отзывов и новостей
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Отзывы и новости»
- [ ] FE: Блок отзывов — карусель
- [ ] FE: Блок отзывов — статичная сетка
- [ ] FE: Блок рейтинга (звёзды + количество отзывов)
@@ -192,6 +233,8 @@
**Цель:** Все формы и диалоги взаимодействия с пациентом.
### Задачи
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации форм и модальных окон
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Формы и контакты»
- [ ] FE: Форма записи — короткая (имя, телефон)
- [ ] FE: Форма записи — расширенная (имя, телефон, специализация, врач, дата)
- [ ] FE: Форма записи в модальном окне
+13 -1
View File
@@ -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-палитрой ожидаемо — цифровые адаптации под экран. | — | Закрыт |
---