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