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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Логотип. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
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() {
|
export default function LogoPage() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto px-8 py-10">
|
<div className="max-w-4xl mx-auto px-8 py-10">
|
||||||
@@ -390,6 +429,55 @@ export default function LogoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,3 +42,109 @@ body {
|
|||||||
color: var(--bb-text);
|
color: var(--bb-text);
|
||||||
-webkit-font-smoothing: antialiased;
|
-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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const NAV: NavSection[] = [
|
|||||||
{
|
{
|
||||||
title: "Компоненты",
|
title: "Компоненты",
|
||||||
items: [
|
items: [
|
||||||
{ label: "Кнопки", href: "/components/buttons", soon: true },
|
{ label: "Кнопки", href: "/components/buttons" },
|
||||||
{ label: "Форм-контролы", href: "/components/forms", soon: true },
|
{ label: "Форм-контролы", href: "/components/forms" },
|
||||||
{ label: "Карточки", href: "/components/cards", soon: true },
|
{ label: "Карточки", href: "/components/cards", soon: true },
|
||||||
{ label: "Бейджи и теги", href: "/components/badges", soon: true },
|
{ label: "Бейджи и теги", href: "/components/badges", soon: true },
|
||||||
{ label: "Алерты", href: "/components/alerts", soon: true },
|
{ label: "Алерты", href: "/components/alerts", soon: true },
|
||||||
@@ -170,7 +170,7 @@ export function Sidebar() {
|
|||||||
color: "var(--bb-sidebar-text-muted)",
|
color: "var(--bb-sidebar-text-muted)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sprint 2 · v0.2.0
|
Sprint 3 · v0.3.0
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<HTMLButtonElement> {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function Button(
|
||||||
|
{ variant = "primary", size = "md", loading = false, disabled, children, className = "", ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const isDisabled = disabled || loading;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
disabled={isDisabled}
|
||||||
|
className={`bb-btn bb-btn-${size} bb-btn-${variant} ${className}`.trim()}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{loading && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 13,
|
||||||
|
height: 13,
|
||||||
|
border: "2px solid currentColor",
|
||||||
|
borderTopColor: "transparent",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "inline-block",
|
||||||
|
animation: "bb-spin 0.65s linear infinite",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function CodeCopy({ code, lang = "jsx" }: { code: string; lang?: string }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ borderRadius: 8, overflow: "hidden", border: "1px solid var(--bb-border)" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
padding: "6px 12px",
|
||||||
|
borderBottom: "1px solid var(--bb-border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--bb-text-muted)", fontFamily: "var(--font-mono)" }}>
|
||||||
|
{lang}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(code);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
background: copied ? "#d1fae5" : "var(--brand-053m)",
|
||||||
|
color: copied ? "#065f46" : "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "3px 10px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontFamily: "var(--font-web)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{copied ? "✓ Скопировано" : "Скопировать"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: "12px 16px",
|
||||||
|
overflowX: "auto",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "var(--font-mono)",
|
||||||
|
color: "var(--bb-text-muted)",
|
||||||
|
background: "#fff",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<code>{code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ToggleProps {
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toggle({ defaultChecked = false, disabled = false, label, onChange }: ToggleProps) {
|
||||||
|
const [checked, setChecked] = useState(defaultChecked);
|
||||||
|
|
||||||
|
function handleToggle() {
|
||||||
|
if (disabled) return;
|
||||||
|
const next = !checked;
|
||||||
|
setChecked(next);
|
||||||
|
onChange?.(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
cursor: disabled ? "not-allowed" : "pointer",
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
onClick={handleToggle}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === " " || e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bb-toggle-track"
|
||||||
|
style={{ background: checked ? "var(--brand-073m)" : "#d1d5db" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bb-toggle-thumb"
|
||||||
|
style={{ transform: checked ? "translateX(20px)" : "translateX(0)" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{label && (
|
||||||
|
<span style={{ fontSize: 14, color: "var(--bb-text)", fontFamily: "var(--font-web)" }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+44
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||||
|
|
||||||
**Версия контекста:** 2.1
|
**Версия контекста:** 3.0
|
||||||
**Дата обновления:** 2026-03-22
|
**Дата обновления:** 2026-03-22
|
||||||
**Актуальный спринт:** Sprint 3
|
**Актуальный спринт:** Sprint 3
|
||||||
**Сайт клиники:** https://oclinica.ru
|
**Сайт клиники:** https://oclinica.ru
|
||||||
@@ -285,8 +285,8 @@ SVG-версия ожидается (не получена от клиники).
|
|||||||
| `/offline/badges` | ✅ Готова | Бейджи |
|
| `/offline/badges` | ✅ Готова | Бейджи |
|
||||||
| `/offline/navigation` | ✅ Готова | Внутренняя навигация |
|
| `/offline/navigation` | ✅ Готова | Внутренняя навигация |
|
||||||
| `/offline/transport` | ✅ Готова | Брендирование транспорта |
|
| `/offline/transport` | ✅ Готова | Брендирование транспорта |
|
||||||
| `/components/buttons` | 🔜 Sprint 3 | Кнопки |
|
| `/components/buttons` | ✅ Готова | Кнопки — все варианты, размеры, состояния |
|
||||||
| `/components/forms` | 🔜 Sprint 3 | Форм-контролы |
|
| `/components/forms` | ✅ Готова | Форм-контролы — Input, Textarea, Select, Checkbox, Radio, Toggle |
|
||||||
| `/components/*` | 🔜 Sprint 3–4 | Карточки, бейджи, алерты, модалки, таблицы |
|
| `/components/*` | 🔜 Sprint 3–4 | Карточки, бейджи, алерты, модалки, таблицы |
|
||||||
| `/blocks/*` | 🔜 Sprint 5–8 | Hero, врачи, отзывы, новости, формы |
|
| `/blocks/*` | 🔜 Sprint 5–8 | Hero, врачи, отзывы, новости, формы |
|
||||||
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты |
|
| `/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 | `<input>` | text, email, password |
|
||||||
|
| Textarea | .bb-textarea | `<textarea>` | многострочный, min-height: 100px |
|
||||||
|
| Select | .bb-select | `<select>` | с кастомной SVG-стрелкой |
|
||||||
|
| Checkbox | .bb-checkbox | `<input type="checkbox">` | 16×16px, accent-color: #5b7b87 |
|
||||||
|
| Radio | .bb-radio | `<input type="radio">` | 16×16px, accent-color: #5b7b87 |
|
||||||
|
| Toggle | .bb-toggle-track | React-компонент `<Toggle>` | 44×24px, track + thumb |
|
||||||
|
|
||||||
|
**Состояния полей:** default (border #e5e7eb) · focus (border #7ecfca + box-shadow) · error (.bb-error, border #dc2626) · disabled (opacity 0.5)
|
||||||
|
|
||||||
|
**Toggle:** выкл → track #d1d5db · вкл → track #5b7b87 (#73M) · thumb: белый круг 20×20px.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 10. Технический стек проекта
|
## 10. Технический стек проекта
|
||||||
|
|
||||||
| Слой | Технология | Версия |
|
| Слой | Технология | Версия |
|
||||||
@@ -361,6 +401,7 @@ SVG-версия ожидается (не получена от клиники).
|
|||||||
| 1.0 | 2026-03-22 | Sprint 1: логотип, базовые цвета |
|
| 1.0 | 2026-03-22 | Sprint 1: логотип, базовые цвета |
|
||||||
| 2.0 | 2026-03-22 | Sprint 2: типографика, оффлайн носители, цвета сайта (8 цветов) |
|
| 2.0 | 2026-03-22 | Sprint 2: типографика, оффлайн носители, цвета сайта (8 цветов) |
|
||||||
| 2.1 | 2026-03-22 | Sprint 2 доп.: +3 цвета сайта (коралловый, светло-жёлтый, светло-зелёный) |
|
| 2.1 | 2026-03-22 | Sprint 2 доп.: +3 цвета сайта (коралловый, светло-жёлтый, светло-зелёный) |
|
||||||
|
| 3.0 | 2026-03-22 | Sprint 3: кнопки (Button), форм-контролы (Input/Textarea/Select/Checkbox/Radio/Toggle), LLM-блок на логотипе |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user