Browse Source

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>
sprint/3
AR 15 M4 1 week ago
parent
commit
c1731615ab
  1. 519
      apps/web/app/components/buttons/page.tsx
  2. 48
      apps/web/app/globals.css
  3. 2
      apps/web/components/ui/Button.tsx

519
apps/web/app/components/buttons/page.tsx

@ -35,114 +35,184 @@ function Section({
); );
} }
const VARIANT_INFO = [ const VARIANTS = [
{ {
variant: "primary" as const, variant: "primary" as const,
label: "Primary", name: "Primary",
label: "Запишитесь к нам",
cssClass: ".bb-btn-primary", cssClass: ".bb-btn-primary",
bg: "#5b7b87", bg: "#FFA39C",
text: "#fff", border: "#FF847B",
description: "Основная кнопка призыва к действию. Тёмный бирюзово-серый фон, белый текст.", textColor: "#fff",
useCase: "Записаться · Подтвердить", radius: "7px",
shadow: "да",
where: "Кнопка отправки форм записи",
example: "«Запишите меня!»",
note: "Коралловый — самый заметный акцент на странице. Всегда один в форме.",
}, },
{ {
variant: "secondary" as const, variant: "outline" as const,
label: "Secondary", name: "Outline",
cssClass: ".bb-btn-secondary", label: "Записаться на приём",
bg: "прозрачный", cssClass: ".bb-btn-outline",
text: "#5b7b87", bg: "#fff",
description: "Второстепенное действие. Контурная кнопка с фирменным цветом.", border: "#BF9975",
useCase: "Узнать подробнее · Редактировать", textColor: "#BF9975",
radius: "7px",
shadow: "нет",
where: "Хедер, навигация, ссылки-кнопки",
example: "«Записаться на прием», «Все новости»",
note: "Бежевая рамка — ненавязчивый вторичный CTA. Не конкурирует с основной формой.",
}, },
{ {
variant: "ghost" as const, variant: "teal" as const,
label: "Ghost", name: "Teal",
cssClass: ".bb-btn-ghost", label: "Позвонить",
bg: "прозрачный", cssClass: ".bb-btn-teal",
text: "#5b7b87", bg: "#60959c",
description: "Третичное действие. Без фона и видимой рамки, текстовый акцент.", border: "прозрачный",
useCase: "Отмена · Назад · Ещё...", textColor: "#fff",
radius: "7px",
shadow: "нет",
where: "Контактные действия — звонок",
example: "«Позвонить»",
note: "Серо-бирюзовый — цвет из реального CSS сайта. Близок к Oracal 066M.",
}, },
{ {
variant: "danger" as const, variant: "pill" as const,
label: "Danger", name: "Pill",
cssClass: ".bb-btn-danger", label: "Заказать звонок",
bg: "#dc2626", cssClass: ".bb-btn-pill",
text: "#fff", bg: "#e9e4d4",
description: "Деструктивные и необратимые действия. Красный фон.", border: "#d5cfbd",
useCase: "Удалить · Отменить запись", textColor: "#333",
radius: "25px",
shadow: "нет",
where: "Модальные триггеры, мягкий CTA",
example: "«Заказать звонок»",
note: "Кремовый фон + pill-форма — мягкий стиль. Используется для открытия модальных окон.",
}, },
]; ];
const LLM_BUTTONS_TEXT = `КНОПКИ — LLM-спецификация const LLM_BUTTONS_TEXT = `КНОПКИ — LLM-спецификация (с реального сайта oclinica.ru)
Версия: v1.0 · /components/buttons Версия: v2.0 · /components/buttons
Источник CSS: perm.oclinica.ru/.../style.css
ВАРИАНТЫ
Вариант | CSS класс | Фон | Текст | Граница | Применение ВАРИАНТЫ (реальный сайт)
primary | .bb-btn-primary | #5b7b87 | #fff | #5b7b87 | Главный CTA: «Записаться», «Подтвердить» Вариант | CSS класс | Фон | Текст | Граница | Radius | Shadow | Применение
secondary | .bb-btn-secondary | прозрачный | #5b7b87 | #5b7b87 | Второстепенное: «Подробнее», «Редактировать» primary | .bb-btn-primary | #FFA39C | #fff | #FF847B | 7px | да | Форм-сабмит «Запишите меня!»
ghost | .bb-btn-ghost | прозрачный | #5b7b87 | нет | Третичное: «Отмена», «Назад» outline | .bb-btn-outline | #fff | #BF9975 | #BF9975 | 7px | нет | Хедер «Записаться на прием», ссылки-кнопки
danger | .bb-btn-danger | #dc2626 | #fff | #dc2626 | Деструктивное: «Удалить», «Отменить запись» teal | .bb-btn-teal | #60959c | #fff | нет | 7px | нет | Звонок «Позвонить»
pill | .bb-btn-pill | #e9e4d4 | #333 | #d5cfbd | 25px | нет | Callback «Заказать звонок»
РАЗМЕРЫ
Размер | CSS класс | padding | font-size | border-radius | Применение CSS С САЙТА (точные значения)
sm | .bb-btn-sm | 5px 12px | 13px | 6px | Компактные интерфейсы, таблицы /* форм-кнопка «Запишите меня!» */
md | .bb-btn-md | 8px 18px | 14px | 8px | Стандарт (по умолчанию) button { background:#FFA39C; color:white; font-weight:bold; border:solid 1px #FF847B;
lg | .bb-btn-lg | 12px 26px | 16px | 10px | Главные CTA на Hero-блоках 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 — «Записаться на прием» */
default без изменений .appointment { background:#FFF; border:#BF9975 solid 1px; color:#BF9975;
hover filter: brightness(0.9) font-size:14px; line-height:38px; padding:3px 12px; border-radius:7px; }
active filter: brightness(0.82)
loading spinner (animation: bb-spin 0.65s linear infinite) + opacity: 0.5 + disabled /* show-phone — «Позвонить» */
disabled opacity: 0.5, cursor: not-allowed .show-phone { background:rgb(96,149,156); color:#fff; border-radius:7px;
font-size:14px; line-height:38px; padding:3px 12px; }
CSS BASE (globals.css)
.bb-btn { font-family: Fira Sans; font-weight: 500; display: inline-flex; align-items: center; gap: 7px; transition: filter 0.15s; } /* callback — «Заказать звонок» */
.bb-btn:hover:not(:disabled) { filter: brightness(0.9); } a.callback_url { background:#e9e4d4; border:#d5cfbd solid 1px; color:#000;
.bb-btn:active:not(:disabled) { filter: brightness(0.82); } border-radius:25px; font-size:16px; padding:6px 18px; }
.bb-btn:disabled { cursor: not-allowed; opacity: 0.5; }
.bb-btn:focus-visible { outline: 2px solid #7ecfca; outline-offset: 2px; } РАЗМЕРЫ (брендбук-компонент)
Размер | 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-кнопки на видимый экран в контексте одной задачи primary (коралловый) только для главного CTA в форме записи
Текст кнопки глагол или чёткий призыв: «Записаться», «Узнать цену» outline (бежевый) хедер, навигация, ссылки-кнопки на странице
Primary главное действие (форма записи, подтверждение) teal (бирюзовый) контактные действия (звонок, направление)
Secondary второстепенное (подробнее, редактировать) pill (кремовый) открытие модальных окон, мягкий callback
Ghost отмена, навигационная ссылка без акцента Не более одного primary на форму
Danger только деструктивные действия (удалить, отменить запись) Не менять цвета вне фирменной палитры сайта
Не менять цвета произвольно только варианты из фирменной палитры Primary не для навигационных ссылок
Не добавлять тени к кнопкам Не накладывать тень на outline/teal/pill`.trim();
Не использовать Danger для нейтральных действий`.trim();
export default function ButtonsPage() { export default function ButtonsPage() {
const codeHtml = `<!-- HTML — базовые классы из globals.css --> const codeHtml = `<!-- Primary — форм-кнопка «Запишите меня!» -->
<button class="bb-btn bb-btn-md bb-btn-primary">Записаться</button> <button class="bb-btn bb-btn-lg bb-btn-primary">Запишите меня!</button>
<button class="bb-btn bb-btn-md bb-btn-secondary">Узнать подробнее</button>
<button class="bb-btn bb-btn-md bb-btn-ghost">Отмена</button> <!-- Outline appointment «Записаться на прием» -->
<button class="bb-btn bb-btn-md bb-btn-danger">Удалить</button> <a class="bb-btn bb-btn-md bb-btn-outline" href="#form">Записаться на прием</a>
<!-- Размеры --> <!-- Teal «Позвонить» -->
<button class="bb-btn bb-btn-sm bb-btn-primary">Маленькая</button> <a class="bb-btn bb-btn-md bb-btn-teal" href="tel:+73422250662">Позвонить</a>
<button class="bb-btn bb-btn-md bb-btn-primary">Средняя</button>
<button class="bb-btn bb-btn-lg bb-btn-primary">Большая</button>`; <!-- Pill «Заказать звонок» -->
<a class="bb-btn bb-btn-md bb-btn-pill" href="#callback">Заказать звонок</a>`;
const codeReact = `import { Button } from "@/components/ui/Button"; const codeReact = `import { Button } from "@/components/ui/Button";
// Варианты // Форм-кнопка (главный CTA)
<Button variant="primary">Записаться</Button> <Button variant="primary" size="lg">Запишите меня!</Button>
<Button variant="secondary">Узнать подробнее</Button>
<Button variant="ghost">Отмена</Button> // Запись из хедера / навигации
<Button variant="danger">Удалить</Button> <Button variant="outline" size="md">Записаться на прием</Button>
// Размеры // Звонок
<Button size="sm">Маленькая</Button> <Button variant="teal" size="md">Позвонить</Button>
<Button size="md">Средняя</Button> {/* по умолчанию */}
<Button size="lg">Большая</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;
}
// Состояния /* Кнопка «Заказать звонок» */
<Button loading>Загрузка...</Button> a.callback_url {
<Button disabled>Недоступно</Button>`; background: #e9e4d4;
border: #d5cfbd solid 1px;
color: #000;
border-radius: 25px;
font-size: 16px;
padding: 6px 18px;
}`;
return ( return (
<div className="max-w-4xl mx-auto px-8 py-10"> <div className="max-w-4xl mx-auto px-8 py-10">
@ -158,46 +228,89 @@ export default function ButtonsPage() {
Кнопки Кнопки
</h1> </h1>
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> <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> </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> </div>
{/* 1. Варианты */} {/* 1. Варианты */}
<Section <Section
id="variants" id="variants"
title="Варианты" title="Варианты"
subtitle="Четыре визуальных типа кнопок для разных контекстов." subtitle="Четыре типа кнопок с реального сайта клиники."
> >
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
{VARIANT_INFO.map(({ variant, label, description, useCase }) => ( {VARIANTS.map(({ variant, name, label, where, example, note, bg, border, textColor, radius, shadow }) => (
<div <div
key={variant} key={variant}
className="rounded-xl border p-5 flex flex-col gap-4" className="rounded-xl border p-5 flex flex-col gap-4"
style={{ borderColor: "var(--bb-border)" }} style={{ borderColor: "var(--bb-border)" }}
> >
{/* Превью */}
<div <div
className="flex items-center justify-center py-6 rounded-lg" className="flex items-center justify-center py-6 rounded-lg"
style={{ background: "var(--bb-sidebar-bg)" }} style={{ background: "var(--bb-sidebar-bg)" }}
> >
<Button variant={variant}>{label}</Button> <Button variant={variant} size="md">
{label}
</Button>
</div> </div>
{/* Инфо */}
<div> <div>
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> <p className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}>
{label} {name}
</p> </p>
<p <p className="text-xs mb-2 leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>
className="text-xs mb-3 leading-relaxed" {note}
style={{ color: "var(--bb-text-muted)" }}
>
{description}
</p> </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 <span
className="inline-block text-[11px] px-2 py-0.5 rounded" key={k}
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }} className="text-[10px] font-mono px-1.5 py-0.5 rounded"
style={{ background: "#f3f4f6", color: "var(--bb-text-muted)" }}
> >
{useCase} {k}: {v}
</span> </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> </div>
))} ))}
@ -208,7 +321,7 @@ export default function ButtonsPage() {
<Section <Section
id="sizes" id="sizes"
title="Размеры" title="Размеры"
subtitle="Три размера для разных уровней иерархии интерфейса." subtitle="Три размера для разных контекстов. lg соответствует форм-кнопке на реальном сайте (18px, bold)."
> >
<div <div
className="rounded-xl border overflow-hidden" className="rounded-xl border overflow-hidden"
@ -219,20 +332,20 @@ export default function ButtonsPage() {
{ {
size: "sm" as const, size: "sm" as const,
label: "Small", label: "Small",
hint: "padding: 5px 12px · font: 13px · radius: 6px", hint: "4px 11px · 13px",
use: "Компактные интерфейсы, действия в таблицах", use: "Компактные контексты, таблицы",
}, },
{ {
size: "md" as const, size: "md" as const,
label: "Medium", label: "Medium",
hint: "padding: 8px 18px · font: 14px · radius: 8px", hint: "8px 16px · 14px",
use: "Стандартный размер (по умолчанию)", use: "Appointment, Teal, Pill (соответствует сайту)",
}, },
{ {
size: "lg" as const, size: "lg" as const,
label: "Large", label: "Large",
hint: "padding: 12px 26px · font: 16px · radius: 10px", hint: "10px 24px · 18px bold",
use: "Главные CTA на Hero-блоках", use: "Primary форм-кнопка (соответствует сайту)",
}, },
] as const ] as const
).map(({ size, label, hint, use }, i) => ( ).map(({ size, label, hint, use }, i) => (
@ -241,7 +354,7 @@ export default function ButtonsPage() {
className="flex items-center gap-6 px-5 py-4" className="flex items-center gap-6 px-5 py-4"
style={{ borderTop: i > 0 ? "1px solid var(--bb-border)" : undefined }} style={{ borderTop: i > 0 ? "1px solid var(--bb-border)" : undefined }}
> >
<div className="w-36 shrink-0"> <div className="w-40 shrink-0">
<Button variant="primary" size={size}> <Button variant="primary" size={size}>
Записаться Записаться
</Button> </Button>
@ -251,12 +364,12 @@ export default function ButtonsPage() {
{label} {label}
</p> </p>
<p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}> <p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
{hint} padding: {hint.split("·")[0].trim()} · font-size: {hint.split("·")[1].trim()}
</p> </p>
</div> </div>
<p <p
className="text-xs hidden lg:block" className="text-xs hidden lg:block"
style={{ color: "var(--bb-text-muted)", maxWidth: 200 }} style={{ color: "var(--bb-text-muted)", maxWidth: 220 }}
> >
{use} {use}
</p> </p>
@ -269,49 +382,53 @@ export default function ButtonsPage() {
<Section <Section
id="states" id="states"
title="Состояния" title="Состояния"
subtitle="Поведение кнопки при разных условиях взаимодействия." subtitle="Базовые состояния кнопки. На реальном сайте hover/transition не определены в CSS."
> >
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{( {(
[ [
{ {
label: "Default", label: "Default",
node: <Button variant="primary">Записаться</Button>, node: <Button variant="primary" size="lg">Записаться</Button>,
hint: "Стандартное состояние", hint: "Стандартное состояние",
}, },
{ {
label: "Hover", label: "Hover",
node: ( node: (
<Button variant="primary" style={{ filter: "brightness(0.9)" }}> <Button
variant="primary"
size="lg"
style={{ filter: "brightness(0.93)" }}
>
Записаться Записаться
</Button> </Button>
), ),
hint: "filter: brightness(0.9) при наведении курсора", hint: "filter: brightness(0.93)",
}, },
{ {
label: "Loading", label: "Loading",
node: <Button variant="primary" loading>Загрузка...</Button>, node: <Button variant="primary" size="lg" loading>Отправка...</Button>,
hint: "Спиннер + opacity: 0.5 + кнопка заблокирована", hint: "Спиннер + blocked",
}, },
{ {
label: "Disabled", label: "Disabled",
node: <Button variant="primary" disabled>Недоступно</Button>, node: <Button variant="primary" size="lg" disabled>Записаться</Button>,
hint: "opacity: 0.5, cursor: not-allowed", hint: "opacity: 0.5",
}, },
] as const ] as const
).map(({ label, node, hint }) => ( ).map(({ label, node, hint }) => (
<div <div
key={label} key={label}
className="rounded-xl border p-5" className="rounded-xl border p-4"
style={{ borderColor: "var(--bb-border)" }} style={{ borderColor: "var(--bb-border)" }}
> >
<div <div
className="flex items-center justify-center py-6 mb-3 rounded-lg" className="flex items-center justify-center py-4 mb-3 rounded-lg"
style={{ background: "var(--bb-sidebar-bg)" }} style={{ background: "var(--bb-sidebar-bg)" }}
> >
{node} {node}
</div> </div>
<p className="text-sm font-medium mb-1" style={{ color: "var(--bb-text)" }}> <p className="text-sm font-medium mb-0.5" style={{ color: "var(--bb-text)" }}>
{label} {label}
</p> </p>
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> <p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
@ -322,51 +439,81 @@ export default function ButtonsPage() {
</div> </div>
</Section> </Section>
{/* 4. Все варианты вместе */} {/* 4. Контекст применения */}
<Section <Section
id="all" id="context"
title="Все варианты и размеры" title="Где применяется"
subtitle="Сводная таблица — визуальное сравнение." subtitle="Таблица: тип кнопки → реальное использование на сайте."
> >
<div <div
className="rounded-xl border overflow-hidden" className="rounded-xl border overflow-hidden"
style={{ borderColor: "var(--bb-border)" }} style={{ borderColor: "var(--bb-border)" }}
> >
<div <table className="w-full text-sm">
className="px-5 py-3 text-xs font-medium border-b" <thead>
style={{ <tr style={{ background: "var(--bb-sidebar-bg)" }}>
color: "var(--bb-text-muted)", {["Вариант", "Цвет фона", "Реальный класс/контекст", "Текст кнопки на сайте"].map((h) => (
background: "var(--bb-sidebar-bg)", <th
borderColor: "var(--bb-border)", key={h}
}} className="text-left px-4 py-3 font-medium text-xs"
style={{ color: "var(--bb-text-muted)" }}
> >
Вариант / Размер {h}
</div> </th>
{VARIANT_INFO.map(({ variant, label }, vi) => ( ))}
<div </tr>
key={variant} </thead>
className="flex items-center gap-5 px-5 py-3" <tbody>
style={{ borderTop: vi > 0 ? "1px solid var(--bb-border)" : undefined }} {[
{
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)" }}
> >
<span <td className="px-4 py-3">{v}</td>
className="w-20 text-xs font-mono shrink-0" <td
className="px-4 py-3 font-mono text-xs"
style={{ color: "var(--bb-text-muted)" }} style={{ color: "var(--bb-text-muted)" }}
> >
{label} {bg}
</span> </td>
<div className="flex items-center gap-3 flex-wrap"> <td
<Button variant={variant} size="sm"> className="px-4 py-3 font-mono text-xs"
Маленькая style={{ color: "var(--bb-text-muted)" }}
</Button> >
<Button variant={variant} size="md"> {ctx}
Средняя </td>
</Button> <td className="px-4 py-3 text-xs" style={{ color: "var(--bb-text)" }}>
<Button variant={variant} size="lg"> {text}
Большая </td>
</Button> </tr>
</div>
</div>
))} ))}
</tbody>
</table>
</div> </div>
</Section> </Section>
@ -374,58 +521,50 @@ export default function ButtonsPage() {
<Section <Section
id="code" id="code"
title="Примеры кода" title="Примеры кода"
subtitle="Скопируйте HTML или JSX для использования в проекте." subtitle="HTML-классы из globals.css, JSX-компонент, и точный CSS с сайта."
> >
<div className="space-y-4"> <div className="space-y-4">
<CodeCopy lang="HTML (CSS-классы из globals.css)" code={codeHtml} /> <CodeCopy lang="HTML (CSS-классы brandbook)" code={codeHtml} />
<CodeCopy lang="JSX (React / Next.js)" code={codeReact} /> <CodeCopy lang="JSX (React / Next.js)" code={codeReact} />
<CodeCopy lang="CSS — точно с сайта oclinica.ru" code={codeSiteExact} />
</div> </div>
</Section> </Section>
{/* LLM-блок */} {/* LLM-блок */}
<LlmBlock path="/components/buttons" version="v1.0" specText={LLM_BUTTONS_TEXT}> <LlmBlock path="/components/buttons" version="v2.0" specText={LLM_BUTTONS_TEXT}>
<LlmSection title="Варианты" /> <LlmSection title="Варианты (реальный сайт oclinica.ru)" />
<LlmTable <LlmTable
headers={["Вариант", "CSS класс", "Фон", "Текст", "Применение"]} headers={["Вариант", "CSS класс", "Фон", "Текст", "Border", "Radius", "Применение"]}
rows={VARIANT_INFO.map((v) => [ rows={VARIANTS.map((v) => [
v.variant, v.variant,
v.cssClass, v.cssClass,
v.bg, v.bg,
v.text, v.textColor,
v.useCase, v.border,
v.radius,
v.where,
])} ])}
/> />
<LlmSection title="Размеры" /> <LlmSection title="Размеры (брендбук-компонент)" />
<LlmTable
headers={["Размер", "CSS класс", "padding", "font-size", "radius", "Применение"]}
rows={[
["sm", ".bb-btn-sm", "5px 12px", "13px", "6px", "Компактные интерфейсы, таблицы"],
["md", ".bb-btn-md", "8px 18px", "14px", "8px", "Стандарт (по умолчанию)"],
["lg", ".bb-btn-lg", "12px 26px", "16px", "10px", "Hero CTA"],
]}
/>
<LlmSection title="Состояния" />
<LlmTable <LlmTable
headers={["Состояние", "CSS / Поведение"]} headers={["Размер", "padding", "font-size", "Применение"]}
rows={[ rows={[
["default", "без изменений"], ["sm", "4px 11px", "13px", "Компактные контексты"],
["hover", "filter: brightness(0.9)"], ["md", "8px 16px", "14px", "Стандарт (outline, teal, pill с сайта)"],
["active", "filter: brightness(0.82)"], ["lg", "10px 24px", "18px bold", "Primary форм-кнопка (соответствует сайту)"],
["loading", "spinner bb-spin + opacity: 0.5 + disabled=true"],
["disabled", "opacity: 0.5 + cursor: not-allowed"],
]} ]}
/> />
<LlmSection title="Правила применения" /> <LlmSection title="Правила применения" />
<LlmRules <LlmRules
rules={[ rules={[
{ ok: true, text: "Не более одной primary-кнопки на один экран в контексте задачи" }, { ok: true, text: "primary (коралловый) — только для submit в формах записи" },
{ ok: true, text: "Текст — глагол или призыв: «Записаться», «Узнать цену»" }, { ok: true, text: "outline (бежевый) — хедер, навигация, второстепенные ссылки" },
{ ok: true, text: "Primary → главное действие формы или подтверждения" }, { ok: true, text: "teal (бирюзовый) — телефонные и контактные действия" },
{ ok: true, text: "Ghost → отмена и навигационные ссылки без акцента" }, { ok: true, text: "pill (кремовый) — открытие модальных окон / callback" },
{ ok: true, text: "Danger → только деструктивные действия" }, { ok: true, text: "Не более одного primary на форму" },
{ ok: false, text: "Не менять цвета произвольно вне фирменной палитры" }, { ok: false, text: "Не менять цвета вне указанной палитры сайта" },
{ ok: false, text: "Не добавлять тени к кнопкам" }, { ok: false, text: "Primary — не для навигационных ссылок" },
{ ok: false, text: "Не использовать Danger для нейтральных действий" }, { ok: false, text: "Не накладывать тень на outline, teal, pill" },
]} ]}
/> />
</LlmBlock> </LlmBlock>

48
apps/web/app/globals.css

@ -68,14 +68,46 @@ body {
.bb-btn:disabled { cursor: not-allowed; opacity: 0.5; } .bb-btn:disabled { cursor: not-allowed; opacity: 0.5; }
.bb-btn:focus-visible { outline: 2px solid var(--brand-053m); outline-offset: 2px; } .bb-btn:focus-visible { outline: 2px solid var(--brand-053m); outline-offset: 2px; }
.bb-btn-sm { font-size: 13px; padding: 5px 12px; border-radius: 6px; border: 1.5px solid transparent; } /* Размеры — только padding и font-size, radius задаётся вариантом */
.bb-btn-md { font-size: 14px; padding: 8px 18px; border-radius: 8px; border: 1.5px solid transparent; } .bb-btn-sm { font-size: 13px; padding: 4px 11px; border: 1px solid transparent; }
.bb-btn-lg { font-size: 16px; padding: 12px 26px; border-radius: 10px; border: 1.5px 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; }
.bb-btn-primary { background: var(--brand-073m); color: #fff; border-color: var(--brand-073m); }
.bb-btn-secondary { background: transparent; color: var(--brand-073m); border-color: var(--brand-073m); } /* Варианты — цвета и радиус по реальному сайту oclinica.ru */
.bb-btn-ghost { background: transparent; color: var(--brand-073m); border-color: transparent; }
.bb-btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; } /* primary — коралловая форм-кнопка «Запишите меня!» */
.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) ───────────────────────────────── */ /* ─── Форм-контролы (Sprint 3) ───────────────────────────────── */
.bb-input, .bb-input,

2
apps/web/components/ui/Button.tsx

@ -2,7 +2,7 @@
import { ButtonHTMLAttributes, forwardRef } from "react"; import { ButtonHTMLAttributes, forwardRef } from "react";
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"; export type ButtonVariant = "primary" | "outline" | "teal" | "pill";
export type ButtonSize = "sm" | "md" | "lg"; export type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {

Loading…
Cancel
Save