feat: LLM-блоки на страницах цветов и типографики + docs/LLM_CONTEXT.md

- Создан компонент components/llm/LlmBlock.tsx (use client):
  LlmBlock, LlmSection, LlmTable, LlmRules — переиспользуемый
  паттерн для LLM-спецификаций на всех страницах брендбука

- /foundation/colors: добавлен раздел «LLM-спецификация» (v2.1)
  · таблица фирменных цветов Oracal (7 шт.)
  · таблица цветов сайта oclinica.ru (11 шт., +3 новых: коралловый,
    светло-жёлтый, светло-зелёный)
  · таблица контрастности WCAG 2.1
  · правила применения ✓/✕
  · кнопка «Скопировать» — plain text для LLM

- /foundation/typography: добавлен раздел «LLM-спецификация» (v2.0)
  · таблица шрифтов DINPro vs Fira Sans
  · шкала DINPro (6 стилей)
  · шкала Fira Sans (11 стилей, включая letter-spacing)
  · применение по носителям + правила ✓/✕

- docs/LLM_CONTEXT.md: создан сводный машиночитаемый файл бренда
  Версия 2.1: все цвета, типографика, логотип, оффлайн, CSS-vars,
  правила — единый контекст для AI при работе с брендом клиники

- docs/TZ.md: добавлено требование ФТ-03-LLM
- docs/SPRINTS.md: задачи LLM-блоков в Sprint 3–8

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-03-22 19:59:58 +05:00
parent 6648f16bcb
commit 0198947c4e
6 changed files with 923 additions and 6 deletions
+175 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { useState, useCallback } from "react";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
import type { Metadata } from "next";
/* ─── Утилиты конвертации ──────────────────────────────────────────── */
@@ -149,6 +150,21 @@ const COLOR_MAPPING = [
web: { name: "Кремовый фон", hex: "#e9e4d4", count: 1 },
note: "Только на сайте — нет Oracal-аналога",
},
{
brand: null,
web: { name: "Коралловый", hex: "#ffa39c", count: 2 },
note: "Только на сайте — CTA-кнопки, нет в оффлайн-палитре",
},
{
brand: null,
web: { name: "Светло-жёлтый фон", hex: "#eef4d1", count: 1 },
note: "Только на сайте — фон карточек отзывов",
},
{
brand: null,
web: { name: "Светло-зелёный фон", hex: "#f2fee6", count: 1 },
note: "Только на сайте — фон секции новостей",
},
];
/* ─── Цвета с сайта ────────────────────────────────────────────────── */
@@ -163,6 +179,9 @@ const WEB_COLORS = [
{ name: "Второстепенный текст", hex: "#949290", usage: "Подписи, второстепенный контент", count: 4, group: "Текст" },
{ name: "Светло-бирюзовый фон", hex: "#b8e6ed", usage: "Фоны светлых секций с акцентом", count: 1, group: "Фоны" },
{ name: "Кремовый фон", hex: "#e9e4d4", usage: "Тёплые фоны секций", count: 1, group: "Фоны" },
{ name: "Коралловый", hex: "#ffa39c", usage: "CTA-кнопки («Запишите меня!»), акцентные призывы к действию", count: 2, group: "Акценты" },
{ name: "Светло-жёлтый фон", hex: "#eef4d1", usage: "Фон карточек отзывов (секция «Отзывы о нас»)", count: 1, group: "Фоны" },
{ name: "Светло-зелёный фон", hex: "#f2fee6", usage: "Фон секции новостей", count: 1, group: "Фоны" },
];
const CONTRAST_PAIRS = [
@@ -381,6 +400,62 @@ function exportTokens() {
URL.revokeObjectURL(url);
}
/* ─── LLM spec text ────────────────────────────────────────────────── */
const LLM_COLORS_TEXT = `# ЦВЕТА — LLM-СПЕЦИФИКАЦИЯ
# Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
# docs/LLM_CONTEXT.md · /foundation/colors · v2.1 · 2026-03-22
ФИРМЕННЫЕ ЦВЕТА (Oracal)
Oracal | Название | HEX | RGB | CSS-переменная | Применение
053M | Основной бирюзовый | #7ECFCA | rgb(126,207,202) | --brand-053m | Акцент, CTA-кнопки, иконки, активные состояния
073M | Тёмный серо-голубой | #5B7B87 | rgb(91,123,135) | --brand-073m | Тёмный фон, хедер, заголовки навигации
066M | Средний бирюзовый | #5BB5AD | rgb(91,181,173) | --brand-066m | Вторичные акценты, фоны секций
050M | Тёмно-синий | #1B4C72 | rgb(27,76,114) | --brand-050m | Наружная реклама, полиграфия, заголовки
081M | Бежевый | #C4A882 | rgb(196,168,130) | --brand-081m | Форма сотрудников, тёплые акценты
080M | Тёмно-коричневый | #5C2E0E | rgb(92,46,14) | --brand-080m | Текст на бежевом, логотип на форме
— | Белый | #FFFFFF | rgb(255,255,255) | --brand-white | Фон, инвертированный текст, логотип на тёмных
ЦВЕТА САЙТА oclinica.ru (CSS: clinic_bootstrap_mobile/css/style.css)
Название | HEX | Группа | × | Применение
Бежевый | #BF9975 | Акценты | 12| Тёплый акцент, фоны, рамки, текст
Серо-бирюзовый | #60959C | Акценты | 7 | Холодный акцент, ссылки
Бирюзовый | #63BAC3 | Акценты | 4 | Фоны акцентных блоков, иконки
Бирюзовый средний | #52B4BD | Акценты | 4 | Вторичные цветовые акценты
Коралловый | #FFA39C | Акценты | 2 | CTA-кнопки («Запишите меня!»)
Основной текст | #464646 | Текст | 3 | Цвет основного текста сайта
Второстепенный текст | #949290 | Текст | 4 | Подписи, второстепенный контент
Светло-бирюзовый фон | #B8E6ED | Фоны | 1 | Фоны светлых секций
Кремовый фон | #E9E4D4 | Фоны | 1 | Тёплые фоны секций
Светло-жёлтый фон | #EEF4D1 | Фоны | 1 | Фон карточек отзывов
Светло-зелёный фон | #F2FEE6 | Фоны | 1 | Фон секции новостей
СООТВЕТСТВИЕ ORACAL → САЙТ
053M #7ECFCA → #63BAC3 (темнее, насыщеннее)
073M #5B7B87 → #60959C (светлее)
066M #5BB5AD → #52B4BD (смещён в синеву)
081M #C4A882 → #BF9975 (темнее, насыщеннее)
050M #1B4C72 → не найден в CSS сайта
080M #5C2E0E → не найден в CSS сайта
КОНТРАСТНОСТЬ WCAG 2.1
#FFFFFF / #5B7B87 | 4.6:1 | AA PASS | AAA FAIL
#FFFFFF / #1B4C72 | 9.3:1 | AA PASS | AAA PASS
#FFFFFF / #5C2E0E | 11.2:1 | AA PASS | AAA PASS
#FFFFFF / #5BB5AD | 3.0:1 | AA FAIL | AAA FAIL | только крупный текст ≥18pt
#111827 / #7ECFCA | 5.8:1 | AA PASS | AAA FAIL
#111827 / #C4A882 | 4.8:1 | AA PASS | AAA FAIL
#5C2E0E / #C4A882 | 6.7:1 | AA PASS | AAA FAIL
ПРАВИЛА
✓ Только цвета из фирменной палитры
✓ Digital → цвета сайта; оффлайн → коды Oracal
✓ Текст на цветном фоне: минимум WCAG AA (4.5:1)
✓ Белый текст на: 073M (#5B7B87), 050M (#1B4C72), 080M (#5C2E0E)
✓ Тёмный текст на: 053M (#7ECFCA), 081M (#C4A882)
✕ Произвольные цвета вне фирменной палитры
✕ Изменение насыщенности / оттенка фирменных цветов
✕ Тёплые и холодные акценты рядом без нейтрального разделителя`.trim();
/* ─── Страница ─────────────────────────────────────────────────────── */
export default function ColorsPage() {
return (
@@ -574,7 +649,7 @@ export default function ColorsPage() {
</section>
{/* 5. Применение */}
<section className="mb-12">
<section className="mb-10">
<div className="mb-6">
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
Правила применения
@@ -635,6 +710,105 @@ export default function ColorsPage() {
</div>
</section>
{/* 6. LLM-спецификация */}
<section className="mb-8">
<div className="mb-6">
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
LLM-спецификация
</h2>
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
Машиночитаемые данные раздела для использования AI-ассистентами при разработке
дизайна, макетов и кода. Нажмите «Скопировать» чтобы получить полный текст.
</p>
</div>
<LlmBlock
path="/foundation/colors"
version="v2.1"
specText={LLM_COLORS_TEXT}
>
{/* Фирменные цвета */}
<div className="space-y-2">
<LlmSection title="Фирменные цвета (Oracal)" />
<LlmTable
headers={["Oracal", "HEX", "RGB", "CSS-переменная", "Применение"]}
rows={BRAND_COLORS.map(c => {
const { r, g, b } = hexToRgb(c.hex);
return [
<span key="o" className="flex items-center gap-1.5">
<span className="w-3 h-3 rounded-sm shrink-0 border inline-block" style={{ background: c.hex, borderColor: "var(--bb-border)" }} />
{c.oracal}
</span>,
<span key="h" style={{ color: "var(--bb-text)" }}>{c.hex.toUpperCase()}</span>,
`rgb(${r},${g},${b})`,
<span key="v" style={{ color: "var(--brand-073m)" }}>{c.cssVar}</span>,
c.usage,
];
})}
/>
</div>
{/* Цвета сайта */}
<div className="space-y-2">
<LlmSection title="Цвета сайта oclinica.ru" />
<LlmTable
headers={["Название", "HEX", "Группа", "×", "Применение"]}
rows={WEB_COLORS.map(c => [
<span key="n" className="flex items-center gap-1.5">
<span className="w-3 h-3 rounded-sm shrink-0 border inline-block" style={{ background: c.hex, borderColor: "var(--bb-border)" }} />
{c.name}
</span>,
<span key="h" style={{ color: "var(--bb-text)" }}>{c.hex.toUpperCase()}</span>,
c.group,
String(c.count),
c.usage,
])}
/>
</div>
{/* Контрастность */}
<div className="space-y-2">
<LlmSection title="Контрастность WCAG 2.1" />
<LlmTable
headers={["Пара (fg / bg)", "Ratio", "AA (4.5:1)", "AAA (7:1)", "AA large (3:1)"]}
rows={CONTRAST_PAIRS.map(p => {
const ratio = contrastRatio(p.fg, p.bg);
const aa = ratio >= 4.5, aaa = ratio >= 7, aal = ratio >= 3;
const badge = (pass: boolean) => (
<span key={String(pass)} style={{ color: pass ? "#059669" : "#dc2626", fontWeight: 700 }}>
{pass ? "PASS" : "FAIL"}
</span>
);
return [
<span key="pair" className="flex items-center gap-1.5">
<span className="w-3 h-3 rounded-sm border inline-block shrink-0" style={{ background: p.bg, borderColor: "var(--bb-border)" }} />
{p.fg} / {p.bg}
</span>,
`${ratio}:1`,
badge(aa),
badge(aaa),
badge(aal),
];
})}
/>
</div>
{/* Правила */}
<div className="space-y-2">
<LlmSection title="Правила применения" />
<LlmRules rules={[
{ ok: true, text: "Только цвета из фирменной палитры" },
{ ok: true, text: "Digital → цвета сайта; оффлайн → коды Oracal" },
{ ok: true, text: "Текст на цветном фоне: минимум WCAG AA (4.5:1)" },
{ ok: true, text: "Белый текст на: 073M, 050M, 080M" },
{ ok: true, text: "Тёмный текст на: 053M, 081M" },
{ ok: false, text: "Произвольные цвета вне фирменной палитры" },
{ ok: false, text: "Изменение насыщенности / оттенка фирменных цветов" },
{ ok: false, text: "Тёплые + холодные акценты рядом без разделителя" },
]} />
</div>
</LlmBlock>
</section>
</div>
);
}
+159
View File
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
export const metadata: Metadata = {
title: "Типографика. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -28,6 +29,60 @@ const FIRA_SCALE = [
{ token: "overline", size: "10px / 0.625rem",weight: "600", lh: "1.4", sample: "ФУНДАМЕНТ → 1.4" },
];
/* ─── LLM spec text ────────────────────────────────────────────────── */
const LLM_TYPOGRAPHY_TEXT = `# ТИПОГРАФИКА — LLM-СПЕЦИФИКАЦИЯ
# Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
# docs/LLM_CONTEXT.md · /foundation/typography · v2.0 · 2026-03-22
ШРИФТЫ БРЕНДА
Шрифт | Тип | Применение | CSS
DINPro | Бренд | Оффлайн, физические носители (бейджи, таблички, транспорт, форма) | font-family: 'DINPro', Arial, sans-serif
Fira Sans| Веб | Сайт, цифровые материалы, брендбук | font-family: 'Fira Sans', sans-serif; Google Fonts; weights: 300/400/500/600
ПРАВИЛО ВЫБОРА: носитель цифровой → Fira Sans; физический/печатный → DINPro
DINPro — ШКАЛА (оффлайн)
Стиль | font-size | rem | weight | line-height
h1 | 40px | 2.5rem | 700 | 1.20
h2 | 32px | 2rem | 700 | 1.25
h3 | 24px | 1.5rem | 700 | 1.30
h4 | 20px | 1.25rem | 700 | 1.35
h5 | 16px | 1rem | 700 | 1.40
h6 | 14px | 0.875rem | 700 | 1.40
Fira Sans — ШКАЛА (веб)
Стиль | font-size | rem | weight | line-height | letter-spacing
h1 | 40px | 2.5rem | 600 | 1.20 | -0.025em
h2 | 32px | 2rem | 600 | 1.25 | -0.020em
h3 | 24px | 1.5rem | 600 | 1.30 | -0.010em
h4 | 20px | 1.25rem | 500 | 1.35 | 0em
h5 | 16px | 1rem | 500 | 1.40 | 0em
h6 | 14px | 0.875rem | 500 | 1.40 | +0.010em
body | 16px | 1rem | 400 | 1.60 | 0em
body-sm | 14px | 0.875rem | 400 | 1.60 | 0em
caption | 12px | 0.75rem | 400 | 1.50 | +0.020em
label | 12px | 0.75rem | 500 | 1.40 | +0.030em
overline | 10px | 0.625rem | 600 | 1.40 | +0.100em (uppercase)
ПРИМЕНЕНИЕ ПО НОСИТЕЛЮ
Носитель | Шрифт
Сайт, цифровые материалы, брендбук | Fira Sans
Форма сотрудников, бейджи | DINPro
Вывески, таблички, навигация | DINPro
Брендирование транспорта | DINPro
Визитки, листовки, полиграфия | DINPro
Telegram-бот, пуш-уведомления | Fira Sans (системный)
ПРАВИЛА
✓ H1 — только один на странице
✓ Не пропускать уровни заголовков (h1 → h2 → h3)
✓ Минимальный размер текста на экране: 12px
✓ Кириллица → Fira Sans (не DINPro)
✓ Fira Sans: доступные веса 300 / 400 / 500 / 600
✕ DINPro на сайте без явного согласования дизайнера
✕ Light (300) для текста < 14px
✕ Смешивать DINPro и Fira Sans на одном носителе`.trim();
/* ─── Компоненты ───────────────────────────────────────────────────── */
function Section({
title,
@@ -344,6 +399,110 @@ export default function TypographyPage() {
</div>
</Section>
{/* LLM-спецификация */}
<section className="mb-8">
<div className="mb-6">
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
LLM-спецификация
</h2>
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
Машиночитаемые данные раздела для использования AI-ассистентами при разработке
дизайна, макетов и кода. Нажмите «Скопировать» чтобы получить полный текст.
</p>
</div>
<LlmBlock
path="/foundation/typography"
version="v2.0"
specText={LLM_TYPOGRAPHY_TEXT}
>
{/* Шрифты */}
<div className="space-y-2">
<LlmSection title="Шрифты бренда" />
<LlmTable
headers={["Шрифт", "Тип", "Применение", "CSS font-family"]}
rows={[
[
<span key="d" style={{ color: "var(--bb-text)", fontWeight: 600 }}>DINPro</span>,
"Бренд",
"Оффлайн, физические носители (бейджи, таблички, транспорт, форма)",
"'DINPro', Arial, sans-serif",
],
[
<span key="f" style={{ color: "var(--bb-text)", fontWeight: 600 }}>Fira Sans</span>,
"Веб",
"Сайт, цифровые материалы, брендбук",
"'Fira Sans', sans-serif · Google Fonts · 300/400/500/600",
],
]}
/>
</div>
{/* DINPro шкала */}
<div className="space-y-2">
<LlmSection title="DINPro — шкала (оффлайн)" />
<LlmTable
headers={["Стиль", "font-size", "rem", "weight", "line-height"]}
rows={DIN_SCALE.map(r => {
const [px, rem] = r.size.split(" / ");
return [r.token, px, rem, r.weight, r.lh];
})}
/>
</div>
{/* Fira Sans шкала */}
<div className="space-y-2">
<LlmSection title="Fira Sans — шкала (веб)" />
<LlmTable
headers={["Стиль", "font-size", "rem", "weight", "line-height", "letter-spacing"]}
rows={[
["h1", "40px", "2.5rem", "600", "1.20", "-0.025em"],
["h2", "32px", "2rem", "600", "1.25", "-0.020em"],
["h3", "24px", "1.5rem", "600", "1.30", "-0.010em"],
["h4", "20px", "1.25rem", "500", "1.35", "0em"],
["h5", "16px", "1rem", "500", "1.40", "0em"],
["h6", "14px", "0.875rem", "500", "1.40", "+0.010em"],
["body", "16px", "1rem", "400", "1.60", "0em"],
["body-sm", "14px", "0.875rem", "400", "1.60", "0em"],
["caption", "12px", "0.75rem", "400", "1.50", "+0.020em"],
["label", "12px", "0.75rem", "500", "1.40", "+0.030em"],
["overline", "10px", "0.625rem", "600", "1.40", "+0.100em"],
]}
/>
</div>
{/* Применение */}
<div className="space-y-2">
<LlmSection title="Применение по носителю" />
<LlmTable
headers={["Носитель", "Шрифт"]}
rows={[
["Сайт, цифровые материалы, брендбук", <span key="fs" style={{ color: "var(--brand-073m)" }}>Fira Sans</span>],
["Форма сотрудников, бейджи", <span key="d1" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
["Вывески, таблички, навигация", <span key="d2" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
["Брендирование транспорта", <span key="d3" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
["Визитки, листовки, полиграфия", <span key="d4" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
["Telegram-бот, пуш-уведомления", <span key="fs2" style={{ color: "var(--brand-073m)" }}>Fira Sans (системный)</span>],
]}
/>
</div>
{/* Правила */}
<div className="space-y-2">
<LlmSection title="Правила применения" />
<LlmRules rules={[
{ ok: true, text: "H1 — только один на странице" },
{ ok: true, text: "Не пропускать уровни заголовков (h1 → h2 → h3)" },
{ ok: true, text: "Минимальный размер текста на экране: 12px" },
{ ok: true, text: "Кириллица → Fira Sans (не DINPro)" },
{ ok: true, text: "Fira Sans: доступные веса 300 / 400 / 500 / 600" },
{ ok: false, text: "DINPro на сайте без явного согласования дизайнера" },
{ ok: false, text: "Light (300) для текста < 14px" },
{ ok: false, text: "Смешивать DINPro и Fira Sans на одном носителе" },
]} />
</div>
</LlmBlock>
</section>
</div>
);
}
+175
View File
@@ -0,0 +1,175 @@
"use client";
import { useState } from "react";
interface LlmBlockProps {
/** Путь страницы, например "/foundation/colors" */
path: string;
/** Версия данных, например "v2.1" */
version: string;
/** Плоский текст для копирования */
specText: string;
/** Содержимое блока — таблицы, правила */
children: React.ReactNode;
}
/**
* LlmBlock — переиспользуемый блок LLM-спецификации.
* Добавляется в конец каждой страницы брендбука, содержащей дизайн-стандарты.
* Требование: ФТ-03-LLM (TZ.md) · docs/LLM_CONTEXT.md
*/
export function LlmBlock({ path, version, specText, children }: LlmBlockProps) {
const [copied, setCopied] = useState(false);
function handleCopy(e: React.MouseEvent) {
e.preventDefault();
navigator.clipboard.writeText(specText);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<details
open
className="rounded-xl overflow-hidden"
style={{
border: "2px dashed var(--brand-053m)",
}}
>
{/* Заголовок */}
<summary
className="flex items-center justify-between px-5 py-3 cursor-pointer select-none list-none"
style={{ background: "rgba(126,207,202,0.07)" }}
>
<div className="flex items-center gap-2 min-w-0">
<span
className="shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded tracking-wider"
style={{ background: "var(--brand-053m)", color: "#fff" }}
>
LLM
</span>
<span className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
LLM-спецификация
</span>
<span
className="text-xs truncate hidden sm:inline"
style={{ color: "var(--bb-text-muted)" }}
>
· машиночитаемые данные · docs/LLM_CONTEXT.md
</span>
</div>
<div className="flex items-center gap-3 shrink-0 ml-3">
<span
className="text-[10px] font-mono hidden md:inline"
style={{ color: "var(--bb-text-muted)" }}
>
{path} · {version}
</span>
<button
onClick={handleCopy}
className="text-xs px-3 py-1 rounded font-medium transition-colors shrink-0"
style={{
background: copied ? "#d1fae5" : "var(--brand-053m)",
color: copied ? "#065f46" : "#fff",
}}
>
{copied ? "✓ Скопировано" : "Скопировать"}
</button>
</div>
</summary>
{/* Содержимое */}
<div
className="px-5 py-5 space-y-6 border-t"
style={{
borderColor: "var(--brand-053m)",
borderStyle: "dashed",
background: "var(--bb-content-bg)",
}}
>
{children}
</div>
</details>
);
}
/* ─── Утилиты для содержимого блока ──────────────────────────── */
/** Заголовок подсекции внутри LLM-блока */
export function LlmSection({ title }: { title: string }) {
return (
<p
className="text-[10px] font-semibold uppercase tracking-widest pb-1 border-b"
style={{ color: "var(--bb-text-muted)", borderColor: "var(--bb-border)" }}
>
{title}
</p>
);
}
/** Компактная таблица для LLM-блока */
export function LlmTable({
headers,
rows,
}: {
headers: string[];
rows: (string | React.ReactNode)[][];
}) {
return (
<div className="overflow-x-auto">
<table className="w-full text-xs border-collapse font-mono">
<thead>
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
{headers.map((h) => (
<th
key={h}
className="text-left px-2 py-1.5 font-medium border whitespace-nowrap"
style={{ color: "var(--bb-text-muted)", borderColor: "var(--bb-border)" }}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, ri) => (
<tr key={ri} style={{ borderTop: `1px solid var(--bb-border)` }}>
{row.map((cell, ci) => (
<td
key={ci}
className="px-2 py-1 border"
style={{
borderColor: "var(--bb-border)",
color: "var(--bb-text-muted)",
maxWidth: "240px",
}}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
/** Список правил ✓ / ✕ */
export function LlmRules({ rules }: { rules: { ok: boolean; text: string }[] }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-0.5">
{rules.map((r) => (
<div key={r.text} className="flex items-start gap-1.5 py-0.5 text-xs font-mono">
<span
style={{ color: r.ok ? "#059669" : "#dc2626", fontWeight: 700, flexShrink: 0 }}
>
{r.ok ? "✓" : "✕"}
</span>
<span style={{ color: "var(--bb-text-muted)" }}>{r.text}</span>
</div>
))}
</div>
);
}