diff --git a/apps/web/app/components/buttons/page.tsx b/apps/web/app/components/buttons/page.tsx
new file mode 100644
index 0000000..35b5d67
--- /dev/null
+++ b/apps/web/app/components/buttons/page.tsx
@@ -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 (
+
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {children}
+
+ );
+}
+
+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 = `
+Записаться
+Узнать подробнее
+Отмена
+Удалить
+
+
+Маленькая
+Средняя
+Большая `;
+
+ const codeReact = `import { Button } from "@/components/ui/Button";
+
+// Варианты
+Записаться
+Узнать подробнее
+Отмена
+Удалить
+
+// Размеры
+Маленькая
+Средняя {/* по умолчанию */}
+Большая
+
+// Состояния
+Загрузка...
+Недоступно `;
+
+ return (
+
+ {/* Заголовок */}
+
+
+ Компоненты → 3.1
+
+
+ Кнопки
+
+
+ Все варианты кнопок, применяемых на сайте клиники.
+ Кнопки — основной элемент призыва к действию в интерфейсе.
+
+
+
+ {/* 1. Варианты */}
+
+
+ {VARIANT_INFO.map(({ variant, label, description, useCase }) => (
+
+
+ {label}
+
+
+
+ {label}
+
+
+ {description}
+
+
+ {useCase}
+
+
+
+ ))}
+
+
+
+ {/* 2. Размеры */}
+
+
+ {(
+ [
+ {
+ 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) => (
+
0 ? "1px solid var(--bb-border)" : undefined }}
+ >
+
+
+ Записаться
+
+
+
+
+ {label}
+
+
+ {hint}
+
+
+
+ {use}
+
+
+ ))}
+
+
+
+ {/* 3. Состояния */}
+
+
+ {(
+ [
+ {
+ label: "Default",
+ node:
Записаться ,
+ hint: "Стандартное состояние",
+ },
+ {
+ label: "Hover",
+ node: (
+
+ Записаться
+
+ ),
+ hint: "filter: brightness(0.9) при наведении курсора",
+ },
+ {
+ label: "Loading",
+ node:
Загрузка... ,
+ hint: "Спиннер + opacity: 0.5 + кнопка заблокирована",
+ },
+ {
+ label: "Disabled",
+ node:
Недоступно ,
+ hint: "opacity: 0.5, cursor: not-allowed",
+ },
+ ] as const
+ ).map(({ label, node, hint }) => (
+
+
+ {node}
+
+
+ {label}
+
+
+ {hint}
+
+
+ ))}
+
+
+
+ {/* 4. Все варианты вместе */}
+
+
+
+ Вариант / Размер
+
+ {VARIANT_INFO.map(({ variant, label }, vi) => (
+
0 ? "1px solid var(--bb-border)" : undefined }}
+ >
+
+ {label}
+
+
+
+ Маленькая
+
+
+ Средняя
+
+
+ Большая
+
+
+
+ ))}
+
+
+
+ {/* 5. Примеры кода */}
+
+
+ {/* LLM-блок */}
+
+
+ [
+ v.variant,
+ v.cssClass,
+ v.bg,
+ v.text,
+ v.useCase,
+ ])}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/app/components/forms/page.tsx b/apps/web/app/components/forms/page.tsx
new file mode 100644
index 0000000..b1882eb
--- /dev/null
+++ b/apps/web/app/components/forms/page.tsx
@@ -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 (
+
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {children}
+
+ );
+}
+
+function FieldLabel({ text, required }: { text: string; required?: boolean }) {
+ return (
+
+ {text}
+ {required && * }
+
+ );
+}
+
+function FieldHint({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+function FieldError({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+function StateCard({
+ label,
+ hint,
+ children,
+}: {
+ label: string;
+ hint: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
{children}
+
+ {label}
+
+
+ {hint}
+
+
+ );
+}
+
+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:
+Состояния: unchecked / checked / disabled / checked+disabled
+
+ПЕРЕКЛЮЧАТЕЛЬ ВАРИАНТА (Radio)
+CSS класс: .bb-radio
+size: 16×16px · accent-color: #5b7b87
+HTML:
+Всегда в группе — один выбранный из нескольких
+
+ТУМБЛЕР (Toggle/Switch)
+Компонент: @/components/ui/Toggle (React, "use client")
+Ширина трека: 44px · Высота: 24px · Бегунок: 20×20px
+Выкл: track #d1d5db · Вкл: track #5b7b87
+CSS: .bb-toggle-track / .bb-toggle-thumb
+HTML-аналог:
+
+ОБЩИЕ ПРАВИЛА
+✓ Метка (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 = `
+Имя пациента *
+
+Укажите имя как в паспорте
+
+
+
+Минимум 3 символа
+
+
+ `;
+
+ const codeTextarea = `Комментарий к записи
+`;
+
+ const codeSelect = `Специализация
+
+ Выберите специализацию
+ ЛОР — ухо, горло, нос
+ Аудиология
+ `;
+
+ const codeCheckbox = `
+
+
+ Согласен с условиями
+
+
+
+
+
+ ЛОР
+
+
+ Аудиология
+
+
`;
+
+ const codeRadio = `
+
+
+ Первичный приём
+
+
+
+ Повторный приём
+
+
+
+ Онлайн-консультация
+
+
`;
+
+ const codeToggle = `import { Toggle } from "@/components/ui/Toggle";
+
+// Базовый тумблер
+
+
+// С меткой
+
+
+// По умолчанию включён
+
+
+// Заблокирован
+ `;
+
+ return (
+
+ {/* Заголовок */}
+
+
+ Компоненты → 3.2
+
+
+ Форм-контролы
+
+
+ Элементы ввода данных: текстовые поля, выпадающие списки, флажки, переключатели.
+ Применяются в формах записи, фильтрах и настройках.
+
+
+
+ {/* 1. Input */}
+
+
+ {/* 2. Textarea */}
+
+
+ {/* 3. Select */}
+
+
+
+
+
+ Выберите специализацию
+ ЛОР — ухо, горло, нос
+ Аудиология
+ ЛОР (детская)
+
+
+
+
+
+
+ Выберите специализацию
+ ЛОР — ухо, горло, нос
+ Аудиология
+ ЛОР (детская)
+
+
+
+
+
+ {/* 4. Checkbox */}
+
+
+ {/* 5. Radio */}
+
+
+
+
+
+ {[
+ { value: "first", label: "Первичный приём", checked: true },
+ { value: "repeat", label: "Повторный приём", checked: false },
+ { value: "online", label: "Онлайн-консультация", checked: false },
+ ].map(({ value, label, checked }) => (
+
+
+
+ {label}
+
+
+ ))}
+
+
+
+
+
+
+ {[
+ { 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}
+
+
+ ))}
+
+
+
+
+
+ {/* 6. Toggle */}
+
+
+
+
+ Интерактивные примеры
+
+
+
+
+
+
+
+
+
+
+
+ Состояния
+
+
+
+ Выкл (default)
+
+
+
+ Вкл (defaultChecked)
+
+
+
+ Disabled
+
+
+
+ Disabled + вкл
+
+
+
+
+
+
+
+ {/* 7. Примеры кода */}
+
+
+ {/* LLM-блок */}
+
+
+ ", "~40px", "Текстовое поле, email, password"],
+ ["Textarea", ".bb-textarea", "
+
+ );
+}
diff --git a/apps/web/app/foundation/logo/page.tsx b/apps/web/app/foundation/logo/page.tsx
index 06536bd..c97319d 100644
--- a/apps/web/app/foundation/logo/page.tsx
+++ b/apps/web/app/foundation/logo/page.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Image from "next/image";
+import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
export const metadata: Metadata = {
title: "Логотип. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -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 (
@@ -390,6 +429,55 @@ export default function LogoPage() {
+ {/* LLM-блок */}
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 3b57c6d..cf1c786 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -42,3 +42,109 @@ 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; }
+
+.bb-btn-sm { font-size: 13px; padding: 5px 12px; border-radius: 6px; border: 1.5px solid transparent; }
+.bb-btn-md { font-size: 14px; padding: 8px 18px; border-radius: 8px; border: 1.5px solid transparent; }
+.bb-btn-lg { font-size: 16px; padding: 12px 26px; border-radius: 10px; border: 1.5px solid transparent; }
+
+.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); }
+.bb-btn-ghost { background: transparent; color: var(--brand-073m); border-color: transparent; }
+.bb-btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; }
+
+/* ─── Форм-контролы (Sprint 3) ───────────────────────────────── */
+.bb-input,
+.bb-textarea,
+.bb-select {
+ font-family: var(--font-web);
+ font-size: 14px;
+ color: var(--bb-text);
+ background: #fff;
+ border: 1.5px solid var(--bb-border);
+ border-radius: 8px;
+ padding: 9px 12px;
+ width: 100%;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ outline: none;
+ -webkit-font-smoothing: antialiased;
+}
+.bb-input:focus,
+.bb-textarea:focus,
+.bb-select:focus {
+ border-color: var(--brand-053m);
+ box-shadow: 0 0 0 3px rgba(126, 207, 202, 0.2);
+}
+.bb-input.bb-error,
+.bb-textarea.bb-error,
+.bb-select.bb-error { border-color: #dc2626; }
+.bb-input.bb-error:focus,
+.bb-textarea.bb-error:focus,
+.bb-select.bb-error:focus { box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); }
+.bb-input:disabled,
+.bb-textarea:disabled,
+.bb-select:disabled { opacity: 0.5; cursor: not-allowed; background: var(--bb-sidebar-bg); }
+
+.bb-textarea { resize: vertical; min-height: 100px; }
+.bb-select {
+ cursor: pointer;
+ appearance: none;
+ 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;
+ padding-right: 36px;
+}
+
+.bb-checkbox,
+.bb-radio {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ accent-color: var(--brand-073m);
+ flex-shrink: 0;
+}
+
+/* ─── Тумблер (Sprint 3) ──────────────────────────────────────── */
+.bb-toggle-track {
+ display: inline-flex;
+ align-items: center;
+ width: 44px;
+ height: 24px;
+ border-radius: 12px;
+ padding: 2px;
+ cursor: pointer;
+ transition: background 0.2s;
+ flex-shrink: 0;
+}
+.bb-toggle-thumb {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
+ transition: transform 0.2s;
+}
diff --git a/apps/web/components/layout/Sidebar.tsx b/apps/web/components/layout/Sidebar.tsx
index b0f3d64..e413abf 100644
--- a/apps/web/components/layout/Sidebar.tsx
+++ b/apps/web/components/layout/Sidebar.tsx
@@ -27,8 +27,8 @@ const NAV: NavSection[] = [
{
title: "Компоненты",
items: [
- { label: "Кнопки", href: "/components/buttons", soon: true },
- { label: "Форм-контролы", href: "/components/forms", soon: true },
+ { label: "Кнопки", href: "/components/buttons" },
+ { label: "Форм-контролы", href: "/components/forms" },
{ label: "Карточки", href: "/components/cards", soon: true },
{ label: "Бейджи и теги", href: "/components/badges", soon: true },
{ label: "Алерты", href: "/components/alerts", soon: true },
@@ -170,7 +170,7 @@ export function Sidebar() {
color: "var(--bb-sidebar-text-muted)",
}}
>
- Sprint 2 · v0.2.0
+ Sprint 3 · v0.3.0
);
diff --git a/apps/web/components/ui/Button.tsx b/apps/web/components/ui/Button.tsx
new file mode 100644
index 0000000..38058e4
--- /dev/null
+++ b/apps/web/components/ui/Button.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { ButtonHTMLAttributes, forwardRef } from "react";
+
+export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
+export type ButtonSize = "sm" | "md" | "lg";
+
+export interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ loading?: boolean;
+}
+
+export const Button = forwardRef(
+ function Button(
+ { variant = "primary", size = "md", loading = false, disabled, children, className = "", ...props },
+ ref
+ ) {
+ const isDisabled = disabled || loading;
+ return (
+
+ {loading && (
+
+ )}
+ {children}
+
+ );
+ }
+);
diff --git a/apps/web/components/ui/CodeCopy.tsx b/apps/web/components/ui/CodeCopy.tsx
new file mode 100644
index 0000000..23b69e7
--- /dev/null
+++ b/apps/web/components/ui/CodeCopy.tsx
@@ -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 (
+
+
+
+ {lang}
+
+ {
+ 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 ? "✓ Скопировано" : "Скопировать"}
+
+
+
+ {code}
+
+
+ );
+}
diff --git a/apps/web/components/ui/Toggle.tsx b/apps/web/components/ui/Toggle.tsx
new file mode 100644
index 0000000..517c763
--- /dev/null
+++ b/apps/web/components/ui/Toggle.tsx
@@ -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 (
+ {
+ if (e.key === " " || e.key === "Enter") {
+ e.preventDefault();
+ handleToggle();
+ }
+ }}
+ >
+
+ {label && (
+
+ {label}
+
+ )}
+
+ );
+}
diff --git a/docs/LLM_CONTEXT.md b/docs/LLM_CONTEXT.md
index 9bd4a28..11c61b7 100644
--- a/docs/LLM_CONTEXT.md
+++ b/docs/LLM_CONTEXT.md
@@ -2,7 +2,7 @@
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
-**Версия контекста:** 2.1
+**Версия контекста:** 3.0
**Дата обновления:** 2026-03-22
**Актуальный спринт:** Sprint 3
**Сайт клиники:** https://oclinica.ru
@@ -285,8 +285,8 @@ SVG-версия ожидается (не получена от клиники).
| `/offline/badges` | ✅ Готова | Бейджи |
| `/offline/navigation` | ✅ Готова | Внутренняя навигация |
| `/offline/transport` | ✅ Готова | Брендирование транспорта |
-| `/components/buttons` | 🔜 Sprint 3 | Кнопки |
-| `/components/forms` | 🔜 Sprint 3 | Форм-контролы |
+| `/components/buttons` | ✅ Готова | Кнопки — все варианты, размеры, состояния |
+| `/components/forms` | ✅ Готова | Форм-контролы — Input, Textarea, Select, Checkbox, Radio, Toggle |
| `/components/*` | 🔜 Sprint 3–4 | Карточки, бейджи, алерты, модалки, таблицы |
| `/blocks/*` | 🔜 Sprint 5–8 | Hero, врачи, отзывы, новости, формы |
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты |
@@ -339,6 +339,46 @@ SVG-версия ожидается (не получена от клиники).
---
+## 9a. Базовые компоненты (Sprint 3)
+
+### Кнопки (Button) · `/components/buttons`
+
+CSS-классы из `globals.css`. Компонент: `@/components/ui/Button` (React, "use client").
+
+| Вариант | CSS класс | Фон | Текст | Граница | Применение |
+|-----------|------------------|------------|---------|-----------|------------|
+| primary | .bb-btn-primary | #5b7b87 | #fff | #5b7b87 | Главный CTA: «Записаться», «Подтвердить» |
+| secondary | .bb-btn-secondary | прозрачный | #5b7b87 | #5b7b87 | Второстепенное действие |
+| ghost | .bb-btn-ghost | прозрачный | #5b7b87 | нет | Третичное действие, отмена |
+| danger | .bb-btn-danger | #dc2626 | #fff | #dc2626 | Деструктивное действие |
+
+| Размер | CSS класс | padding | font-size | border-radius |
+|--------|------------|------------|-----------|---------------|
+| sm | .bb-btn-sm | 5px 12px | 13px | 6px |
+| md | .bb-btn-md | 8px 18px | 14px | 8px |
+| lg | .bb-btn-lg | 12px 26px | 16px | 10px |
+
+**Состояния:** default · hover (brightness 0.9) · active (brightness 0.82) · loading (spinner + opacity 0.5) · disabled (opacity 0.5)
+
+**Правила:** не более одной primary на экран в контексте задачи · текст — глагол или призыв · Danger только для деструктивных действий.
+
+### Форм-контролы (Forms) · `/components/forms`
+
+| Элемент | CSS класс | Тег HTML | Описание |
+|----------|-----------------|----------------------------|----------|
+| Input | .bb-input | ` ` | text, email, password |
+| Textarea | .bb-textarea | `