Browse Source
- 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>sprint/3
9 changed files with 1428 additions and 6 deletions
@ -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> |
||||||
|
); |
||||||
|
} |
||||||
@ -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> |
||||||
|
); |
||||||
|
} |
||||||
Loading…
Reference in new issue