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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user