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>
This commit is contained in:
AR 15 M4
2026-03-22 20:20:41 +05:00
parent 0198947c4e
commit 0855892643
9 changed files with 1428 additions and 6 deletions
+434
View File
@@ -0,0 +1,434 @@
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 VARIANT_INFO = [
{
variant: "primary" as const,
label: "Primary",
cssClass: ".bb-btn-primary",
bg: "#5b7b87",
text: "#fff",
description: "Основная кнопка призыва к действию. Тёмный бирюзово-серый фон, белый текст.",
useCase: "Записаться · Подтвердить",
},
{
variant: "secondary" as const,
label: "Secondary",
cssClass: ".bb-btn-secondary",
bg: "прозрачный",
text: "#5b7b87",
description: "Второстепенное действие. Контурная кнопка с фирменным цветом.",
useCase: "Узнать подробнее · Редактировать",
},
{
variant: "ghost" as const,
label: "Ghost",
cssClass: ".bb-btn-ghost",
bg: "прозрачный",
text: "#5b7b87",
description: "Третичное действие. Без фона и видимой рамки, текстовый акцент.",
useCase: "Отмена · Назад · Ещё...",
},
{
variant: "danger" as const,
label: "Danger",
cssClass: ".bb-btn-danger",
bg: "#dc2626",
text: "#fff",
description: "Деструктивные и необратимые действия. Красный фон.",
useCase: "Удалить · Отменить запись",
},
];
const LLM_BUTTONS_TEXT = `КНОПКИ — LLM-спецификация
Версия: v1.0 · /components/buttons
ВАРИАНТЫ
Вариант | CSS класс | Фон | Текст | Граница | Применение
primary | .bb-btn-primary | #5b7b87 | #fff | #5b7b87 | Главный CTA: «Записаться», «Подтвердить»
secondary | .bb-btn-secondary | прозрачный | #5b7b87 | #5b7b87 | Второстепенное: «Подробнее», «Редактировать»
ghost | .bb-btn-ghost | прозрачный | #5b7b87 | нет | Третичное: «Отмена», «Назад»
danger | .bb-btn-danger | #dc2626 | #fff | #dc2626 | Деструктивное: «Удалить», «Отменить запись»
РАЗМЕРЫ
Размер | CSS класс | padding | font-size | border-radius | Применение
sm | .bb-btn-sm | 5px 12px | 13px | 6px | Компактные интерфейсы, таблицы
md | .bb-btn-md | 8px 18px | 14px | 8px | Стандарт (по умолчанию)
lg | .bb-btn-lg | 12px 26px | 16px | 10px | Главные CTA на Hero-блоках
СОСТОЯНИЯ
default — без изменений
hover — filter: brightness(0.9)
active — filter: brightness(0.82)
loading — spinner (animation: bb-spin 0.65s linear infinite) + opacity: 0.5 + disabled
disabled — opacity: 0.5, cursor: not-allowed
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; }
.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 #7ecfca; outline-offset: 2px; }
ПРАВИЛА ПРИМЕНЕНИЯ
✓ Не более одной primary-кнопки на видимый экран в контексте одной задачи
✓ Текст кнопки — глагол или чёткий призыв: «Записаться», «Узнать цену»
✓ Primary → главное действие (форма записи, подтверждение)
✓ Secondary → второстепенное (подробнее, редактировать)
✓ Ghost → отмена, навигационная ссылка без акцента
✓ Danger → только деструктивные действия (удалить, отменить запись)
✕ Не менять цвета произвольно — только варианты из фирменной палитры
✕ Не добавлять тени к кнопкам
✕ Не использовать Danger для нейтральных действий`.trim();
export default function ButtonsPage() {
const codeHtml = `<!-- HTML — базовые классы из globals.css -->
<button class="bb-btn bb-btn-md 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>
<button class="bb-btn bb-btn-md bb-btn-danger">Удалить</button>
<!-- Размеры -->
<button class="bb-btn bb-btn-sm bb-btn-primary">Маленькая</button>
<button class="bb-btn bb-btn-md bb-btn-primary">Средняя</button>
<button class="bb-btn bb-btn-lg bb-btn-primary">Большая</button>`;
const codeReact = `import { Button } from "@/components/ui/Button";
// Варианты
<Button variant="primary">Записаться</Button>
<Button variant="secondary">Узнать подробнее</Button>
<Button variant="ghost">Отмена</Button>
<Button variant="danger">Удалить</Button>
// Размеры
<Button size="sm">Маленькая</Button>
<Button size="md">Средняя</Button> {/* по умолчанию */}
<Button size="lg">Большая</Button>
// Состояния
<Button loading>Загрузка...</Button>
<Button disabled>Недоступно</Button>`;
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)" }}>
Все варианты кнопок, применяемых на сайте клиники.
Кнопки основной элемент призыва к действию в интерфейсе.
</p>
</div>
{/* 1. Варианты */}
<Section
id="variants"
title="Варианты"
subtitle="Четыре визуальных типа кнопок для разных контекстов."
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{VARIANT_INFO.map(({ variant, label, description, useCase }) => (
<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}>{label}</Button>
</div>
<div>
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
{label}
</p>
<p
className="text-xs mb-3 leading-relaxed"
style={{ color: "var(--bb-text-muted)" }}
>
{description}
</p>
<span
className="inline-block text-[11px] px-2 py-0.5 rounded"
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }}
>
{useCase}
</span>
</div>
</div>
))}
</div>
</Section>
{/* 2. Размеры */}
<Section
id="sizes"
title="Размеры"
subtitle="Три размера для разных уровней иерархии интерфейса."
>
<div
className="rounded-xl border overflow-hidden"
style={{ borderColor: "var(--bb-border)" }}
>
{(
[
{
size: "sm" as const,
label: "Small",
hint: "padding: 5px 12px · font: 13px · radius: 6px",
use: "Компактные интерфейсы, действия в таблицах",
},
{
size: "md" as const,
label: "Medium",
hint: "padding: 8px 18px · font: 14px · radius: 8px",
use: "Стандартный размер (по умолчанию)",
},
{
size: "lg" as const,
label: "Large",
hint: "padding: 12px 26px · font: 16px · radius: 10px",
use: "Главные CTA на Hero-блоках",
},
] 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-36 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)" }}>
{hint}
</p>
</div>
<p
className="text-xs hidden lg:block"
style={{ color: "var(--bb-text-muted)", maxWidth: 200 }}
>
{use}
</p>
</div>
))}
</div>
</Section>
{/* 3. Состояния */}
<Section
id="states"
title="Состояния"
subtitle="Поведение кнопки при разных условиях взаимодействия."
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{(
[
{
label: "Default",
node: <Button variant="primary">Записаться</Button>,
hint: "Стандартное состояние",
},
{
label: "Hover",
node: (
<Button variant="primary" style={{ filter: "brightness(0.9)" }}>
Записаться
</Button>
),
hint: "filter: brightness(0.9) при наведении курсора",
},
{
label: "Loading",
node: <Button variant="primary" loading>Загрузка...</Button>,
hint: "Спиннер + opacity: 0.5 + кнопка заблокирована",
},
{
label: "Disabled",
node: <Button variant="primary" disabled>Недоступно</Button>,
hint: "opacity: 0.5, cursor: not-allowed",
},
] as const
).map(({ label, node, hint }) => (
<div
key={label}
className="rounded-xl border p-5"
style={{ borderColor: "var(--bb-border)" }}
>
<div
className="flex items-center justify-center py-6 mb-3 rounded-lg"
style={{ background: "var(--bb-sidebar-bg)" }}
>
{node}
</div>
<p className="text-sm font-medium mb-1" 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="all"
title="Все варианты и размеры"
subtitle="Сводная таблица — визуальное сравнение."
>
<div
className="rounded-xl border overflow-hidden"
style={{ borderColor: "var(--bb-border)" }}
>
<div
className="px-5 py-3 text-xs font-medium border-b"
style={{
color: "var(--bb-text-muted)",
background: "var(--bb-sidebar-bg)",
borderColor: "var(--bb-border)",
}}
>
Вариант / Размер
</div>
{VARIANT_INFO.map(({ variant, label }, vi) => (
<div
key={variant}
className="flex items-center gap-5 px-5 py-3"
style={{ borderTop: vi > 0 ? "1px solid var(--bb-border)" : undefined }}
>
<span
className="w-20 text-xs font-mono shrink-0"
style={{ color: "var(--bb-text-muted)" }}
>
{label}
</span>
<div className="flex items-center gap-3 flex-wrap">
<Button variant={variant} size="sm">
Маленькая
</Button>
<Button variant={variant} size="md">
Средняя
</Button>
<Button variant={variant} size="lg">
Большая
</Button>
</div>
</div>
))}
</div>
</Section>
{/* 5. Примеры кода */}
<Section
id="code"
title="Примеры кода"
subtitle="Скопируйте HTML или JSX для использования в проекте."
>
<div className="space-y-4">
<CodeCopy lang="HTML (CSS-классы из globals.css)" code={codeHtml} />
<CodeCopy lang="JSX (React / Next.js)" code={codeReact} />
</div>
</Section>
{/* LLM-блок */}
<LlmBlock path="/components/buttons" version="v1.0" specText={LLM_BUTTONS_TEXT}>
<LlmSection title="Варианты" />
<LlmTable
headers={["Вариант", "CSS класс", "Фон", "Текст", "Применение"]}
rows={VARIANT_INFO.map((v) => [
v.variant,
v.cssClass,
v.bg,
v.text,
v.useCase,
])}
/>
<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
headers={["Состояние", "CSS / Поведение"]}
rows={[
["default", "без изменений"],
["hover", "filter: brightness(0.9)"],
["active", "filter: brightness(0.82)"],
["loading", "spinner bb-spin + opacity: 0.5 + disabled=true"],
["disabled", "opacity: 0.5 + cursor: not-allowed"],
]}
/>
<LlmSection title="Правила применения" />
<LlmRules
rules={[
{ ok: true, text: "Не более одной primary-кнопки на один экран в контексте задачи" },
{ ok: true, text: "Текст — глагол или призыв: «Записаться», «Узнать цену»" },
{ ok: true, text: "Primary → главное действие формы или подтверждения" },
{ ok: true, text: "Ghost → отмена и навигационные ссылки без акцента" },
{ ok: true, text: "Danger → только деструктивные действия" },
{ ok: false, text: "Не менять цвета произвольно вне фирменной палитры" },
{ ok: false, text: "Не добавлять тени к кнопкам" },
{ ok: false, text: "Не использовать Danger для нейтральных действий" },
]}
/>
</LlmBlock>
</div>
);
}
+589
View File
@@ -0,0 +1,589 @@
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-спецификация
Версия: v1.0 · /components/forms
ТЕКСТОВОЕ ПОЛЕ (Input)
CSS класс: .bb-input
Высота: ~40px (padding: 9px 12px)
border: 1.5px solid #e5e7eb · border-radius: 8px · font: Fira Sans 14px
Состояния:
default: border #e5e7eb
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
ВЫПАДАЮЩИЙ СПИСОК (Select)
CSS класс: .bb-select
Стрелка: SVG background-image (data URI) · padding-right: 36px
Те же состояния что у 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" />
ОБЩИЕ ПРАВИЛА
✓ Метка (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="Настройка недоступна" />`;
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."
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<StateCard label="Default" hint="Стандартное состояние">
<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."
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<StateCard label="Default" hint="Стандартное состояние · 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."
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<StateCard label="Default" hint="Нативный select с кастомной стрелкой">
<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="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} />
</div>
</Section>
{/* LLM-блок */}
<LlmBlock path="/components/forms" version="v1.0" specText={LLM_FORMS_TEXT}>
<LlmSection title="Элементы ввода" />
<LlmTable
headers={["Элемент", "CSS класс", "Тег", "Высота", "Описание"]}
rows={[
["Input", ".bb-input", "<input>", "~40px", "Текстовое поле, email, password"],
["Textarea", ".bb-textarea", "<textarea>", "≥100px", "Многострочный ввод, resize:vertical"],
["Select", ".bb-select", "<select>", "~40px", "Выбор из списка, кастомная стрелка"],
["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: 1.5px solid #e5e7eb"],
["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>
);
}