Compare commits
No commits in common. 'main' and 'sprint/2' have entirely different histories.
@ -1,53 +0,0 @@ |
|||||||
-- CreateEnum |
|
||||||
CREATE TYPE "Role" AS ENUM ('viewer', 'editor'); |
|
||||||
|
|
||||||
-- CreateEnum |
|
||||||
CREATE TYPE "ComponentStatus" AS ENUM ('draft', 'review', 'approved'); |
|
||||||
|
|
||||||
-- CreateTable |
|
||||||
CREATE TABLE "User" ( |
|
||||||
"id" TEXT NOT NULL, |
|
||||||
"email" TEXT NOT NULL, |
|
||||||
"name" TEXT NOT NULL, |
|
||||||
"passwordHash" TEXT NOT NULL, |
|
||||||
"role" "Role" NOT NULL DEFAULT 'viewer', |
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
|
||||||
|
|
||||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id") |
|
||||||
); |
|
||||||
|
|
||||||
-- CreateTable |
|
||||||
CREATE TABLE "ExperimentalComponent" ( |
|
||||||
"id" TEXT NOT NULL, |
|
||||||
"name" TEXT NOT NULL, |
|
||||||
"baseComponent" TEXT NOT NULL, |
|
||||||
"attributes" JSONB NOT NULL, |
|
||||||
"status" "ComponentStatus" NOT NULL DEFAULT 'draft', |
|
||||||
"authorId" TEXT NOT NULL, |
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL, |
|
||||||
|
|
||||||
CONSTRAINT "ExperimentalComponent_pkey" PRIMARY KEY ("id") |
|
||||||
); |
|
||||||
|
|
||||||
-- CreateTable |
|
||||||
CREATE TABLE "Block" ( |
|
||||||
"id" TEXT NOT NULL, |
|
||||||
"path" TEXT NOT NULL, |
|
||||||
"name" TEXT NOT NULL, |
|
||||||
"version" TEXT NOT NULL, |
|
||||||
"isInPreview" BOOLEAN NOT NULL DEFAULT false, |
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL, |
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
|
||||||
|
|
||||||
CONSTRAINT "Block_pkey" PRIMARY KEY ("id") |
|
||||||
); |
|
||||||
|
|
||||||
-- CreateIndex |
|
||||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); |
|
||||||
|
|
||||||
-- CreateIndex |
|
||||||
CREATE UNIQUE INDEX "Block_path_key" ON "Block"("path"); |
|
||||||
|
|
||||||
-- AddForeignKey |
|
||||||
ALTER TABLE "ExperimentalComponent" ADD CONSTRAINT "ExperimentalComponent_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; |
|
||||||
@ -1,2 +0,0 @@ |
|||||||
-- AlterTable |
|
||||||
ALTER TABLE "Block" ADD COLUMN "changelog" JSONB NOT NULL DEFAULT '[]'; |
|
||||||
@ -1,3 +0,0 @@ |
|||||||
# Please do not edit this file manually |
|
||||||
# It should be added in your version-control system (e.g., Git) |
|
||||||
provider = "postgresql" |
|
||||||
@ -1,134 +0,0 @@ |
|||||||
import 'dotenv/config'; |
|
||||||
import { PrismaPg } from '@prisma/adapter-pg'; |
|
||||||
import { PrismaClient } from '@prisma/client'; |
|
||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); |
|
||||||
const prisma = new PrismaClient({ adapter }); |
|
||||||
|
|
||||||
const BLOCKS = [ |
|
||||||
{ |
|
||||||
path: '/blocks/navigation', |
|
||||||
name: 'Шапка / Навигация', |
|
||||||
version: 'v1.0', |
|
||||||
isInPreview: true, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.0', date: '23.03.2026', changes: ['Топ-бар, логотип, главное меню из 8 пунктов'] }, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/hero', |
|
||||||
name: 'Hero-баннер', |
|
||||||
version: 'v1.2', |
|
||||||
isInPreview: true, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.2', date: '24.03.2026', changes: [ |
|
||||||
'H1: цвет #cb9768, размер 36px (было ~20px #53514e)', |
|
||||||
'Заголовок баннера: 22px #333 (было 16px #111827)', |
|
||||||
'CTA-кнопка: pill-стиль (было outline)', |
|
||||||
'Дефис в H1: «–» → «-»', |
|
||||||
]}, |
|
||||||
{ version: 'v1.1', date: '23.03.2026', changes: [ |
|
||||||
'Единый фон #f9f4e7 (ранее разбит на две зоны)', |
|
||||||
'Реальное фото врача с пациентом', |
|
||||||
]}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/ceo', |
|
||||||
name: 'Вводный текст (CEO-блок)', |
|
||||||
version: 'v1.0', |
|
||||||
isInPreview: false, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.0', date: '23.03.2026', changes: ['Текст специализации клиники, вопросы-стимулы'] }, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/doctors', |
|
||||||
name: 'Наши врачи', |
|
||||||
version: 'v1.2', |
|
||||||
isInPreview: true, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.2', date: '24.03.2026', changes: [ |
|
||||||
'H2: 36px #000000 (было ~30px #111827)', |
|
||||||
'line-height: 38px', |
|
||||||
]}, |
|
||||||
{ version: 'v1.1', date: '23.03.2026', changes: [ |
|
||||||
'6 реальных фото врачей с сайта', |
|
||||||
'Статистика без фона, только border-bottom #60959c', |
|
||||||
]}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/reviews', |
|
||||||
name: 'Отзывы', |
|
||||||
version: 'v1.1', |
|
||||||
isInPreview: true, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.1', date: '24.03.2026', changes: [ |
|
||||||
'H2: 36px #000000 (было ~20px #111827)', |
|
||||||
'line-height: 38px', |
|
||||||
]}, |
|
||||||
{ version: 'v1.0', date: '23.03.2026', changes: [ |
|
||||||
'Карусель отзывов: кавычка, текст, стрелки', |
|
||||||
]}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/contact-forms', |
|
||||||
name: 'Формы записи', |
|
||||||
version: 'v1.1', |
|
||||||
isInPreview: true, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.1', date: '24.03.2026', changes: [ |
|
||||||
'H2: 36px #000000', |
|
||||||
'Фон формы 1: #b8e6ed → #d4f6f8', |
|
||||||
'Фон формы 2: #ffffff → #d4f6f8 (обе формы на одном фоне)', |
|
||||||
]}, |
|
||||||
{ version: 'v1.0', date: '23.03.2026', changes: [ |
|
||||||
'Две формы записи: «Будьте здоровы!» и «Узнайте стоимость операции»', |
|
||||||
]}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/news', |
|
||||||
name: 'Новости', |
|
||||||
version: 'v1.1', |
|
||||||
isInPreview: true, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.1', date: '24.03.2026', changes: [ |
|
||||||
'H2: 36px #000000', |
|
||||||
'Фон секции: #fff → #f2fee6 (светло-зелёный)', |
|
||||||
]}, |
|
||||||
{ version: 'v1.0', date: '23.03.2026', changes: [ |
|
||||||
'4 карточки новостей в ряд, кнопка «Все новости»', |
|
||||||
]}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
path: '/blocks/contact', |
|
||||||
name: 'Подвал / Контакт', |
|
||||||
version: 'v1.0', |
|
||||||
isInPreview: false, |
|
||||||
changelog: [ |
|
||||||
{ version: 'v1.0', date: '23.03.2026', changes: ['4 колонки ссылок, адрес, часы работы'] }, |
|
||||||
], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
async function main() { |
|
||||||
for (const block of BLOCKS) { |
|
||||||
await prisma.block.upsert({ |
|
||||||
where: { path: block.path }, |
|
||||||
update: { version: block.version, isInPreview: block.isInPreview, changelog: block.changelog }, |
|
||||||
create: block, |
|
||||||
}); |
|
||||||
} |
|
||||||
console.log(`Seeded ${BLOCKS.length} blocks`); |
|
||||||
} |
|
||||||
|
|
||||||
main() |
|
||||||
.catch((e) => { |
|
||||||
console.error(e); |
|
||||||
process.exit(1); |
|
||||||
}) |
|
||||||
.finally(() => prisma.$disconnect()); |
|
||||||
@ -1,25 +0,0 @@ |
|||||||
import { Controller, Get, Patch, Query, Body } from '@nestjs/common'; |
|
||||||
import { BlocksService } from './blocks.service'; |
|
||||||
|
|
||||||
@Controller('blocks') |
|
||||||
export class BlocksController { |
|
||||||
constructor(private blocks: BlocksService) {} |
|
||||||
|
|
||||||
@Get() |
|
||||||
findAll() { |
|
||||||
return this.blocks.findAll(); |
|
||||||
} |
|
||||||
|
|
||||||
@Get('by-path') |
|
||||||
findByPath(@Query('path') path: string) { |
|
||||||
return this.blocks.findByPath(path); |
|
||||||
} |
|
||||||
|
|
||||||
@Patch('by-path') |
|
||||||
update( |
|
||||||
@Query('path') path: string, |
|
||||||
@Body() body: { version?: string; isInPreview?: boolean; changelog?: object[] }, |
|
||||||
) { |
|
||||||
return this.blocks.update(path, body); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,10 +0,0 @@ |
|||||||
import { Module } from '@nestjs/common'; |
|
||||||
import { BlocksController } from './blocks.controller'; |
|
||||||
import { BlocksService } from './blocks.service'; |
|
||||||
import { PrismaService } from '../prisma/prisma.service'; |
|
||||||
|
|
||||||
@Module({ |
|
||||||
controllers: [BlocksController], |
|
||||||
providers: [BlocksService, PrismaService], |
|
||||||
}) |
|
||||||
export class BlocksModule {} |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
import { Injectable } from '@nestjs/common'; |
|
||||||
import { PrismaService } from '../prisma/prisma.service'; |
|
||||||
|
|
||||||
@Injectable() |
|
||||||
export class BlocksService { |
|
||||||
constructor(private prisma: PrismaService) {} |
|
||||||
|
|
||||||
findAll() { |
|
||||||
return this.prisma.block.findMany({ orderBy: { createdAt: 'asc' } }); |
|
||||||
} |
|
||||||
|
|
||||||
async findByPath(path: string) { |
|
||||||
const existing = await this.prisma.block.findUnique({ where: { path } }); |
|
||||||
if (existing) return existing; |
|
||||||
return this.prisma.block.create({ |
|
||||||
data: { path, name: path, version: 'v0.1', isInPreview: false }, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
update(path: string, data: { version?: string; isInPreview?: boolean; changelog?: object[] }) { |
|
||||||
return this.prisma.block.update({ where: { path }, data }); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,10 +1,8 @@ |
|||||||
import 'dotenv/config'; |
|
||||||
import { NestFactory } from '@nestjs/core'; |
import { NestFactory } from '@nestjs/core'; |
||||||
import { AppModule } from './app.module'; |
import { AppModule } from './app.module'; |
||||||
|
|
||||||
async function bootstrap() { |
async function bootstrap() { |
||||||
const app = await NestFactory.create(AppModule); |
const app = await NestFactory.create(AppModule); |
||||||
app.enableCors({ origin: [/^http:\/\/localhost:\d+$/] }); |
|
||||||
await app.listen(process.env.PORT ?? 3000); |
await app.listen(process.env.PORT ?? 3000); |
||||||
} |
} |
||||||
bootstrap(); |
bootstrap(); |
||||||
|
|||||||
@ -1,19 +0,0 @@ |
|||||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; |
|
||||||
import { PrismaPg } from '@prisma/adapter-pg'; |
|
||||||
import { PrismaClient } from '@prisma/client'; |
|
||||||
|
|
||||||
@Injectable() |
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { |
|
||||||
constructor() { |
|
||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); |
|
||||||
super({ adapter }); |
|
||||||
} |
|
||||||
|
|
||||||
async onModuleInit() { |
|
||||||
await this.$connect(); |
|
||||||
} |
|
||||||
|
|
||||||
async onModuleDestroy() { |
|
||||||
await this.$disconnect(); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,190 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { CeoBlock, CEO_QUESTIONS } from "@/components/blocks/CeoBlock"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Вводный текст (CEO-блок). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const LLM_CEO_TEXT = ` |
|
||||||
БЛОК: Вводный текст клиники (CEO-блок) |
|
||||||
Источник: perm.oclinica.ru/lor — секция после баннера |
|
||||||
Версия: v1.0 |
|
||||||
|
|
||||||
НАЗНАЧЕНИЕ: |
|
||||||
Информационный блок под hero-баннером. Рассказывает о специализации клиники |
|
||||||
и вовлекает пациента через вопросы-стимулы. |
|
||||||
|
|
||||||
СТРУКТУРА: |
|
||||||
1. Вводный абзац |
|
||||||
«Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей |
|
||||||
с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах: ул. Цитная, 9, ул. Г. Звезда, 31а...» |
|
||||||
|
|
||||||
2. Вопросы-стимулы (формат: «— Вопрос?») |
|
||||||
Каждый вопрос — отдельный абзац с тире, связанный с симптомами пациентов: |
|
||||||
— У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину? |
|
||||||
— Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»? |
|
||||||
— Заболел ребёнок? |
|
||||||
— Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых... |
|
||||||
— Вам назначили проведение эндоскопической операции на ухе, горле или носе? |
|
||||||
|
|
||||||
3. Заключительный абзац |
|
||||||
«Обращайтесь в ЛОР центр ухо, горло, нос в Перми...» |
|
||||||
«Будьте здоровы!» |
|
||||||
|
|
||||||
ТИПОГРАФИКА: |
|
||||||
Шрифт: Fira Sans, 14px, line-height 1.6–1.8 |
|
||||||
Цвет текста: #374151 (#bb-text) |
|
||||||
Вопросы: тот же стиль, начинаются с «—» |
|
||||||
Ключевые слова в тексте — обычно ссылки синего цвета (#0089c3) |
|
||||||
Фон блока: #ffffff, отступы 40–60px сверху и снизу |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ Вопросы начинаются с «—» (тире) |
|
||||||
✓ Ключевые медицинские термины — ссылки #0089c3 |
|
||||||
✓ Текст без H2 заголовка — просто связный параграф |
|
||||||
✕ Не добавлять маркированные списки (только тире) |
|
||||||
✕ Не менять стиль вопросов на другой формат |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
|
|
||||||
export default function CeoPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Вводный текст (CEO-блок) |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/ceo" defaultVersion="v1.0" defaultIsInPreview={false} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Блок после hero-баннера на perm.oclinica.ru/lor. Описание специализации клиники |
|
||||||
+ вопросы-стимулы для пациентов. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<div className="rounded-xl overflow-hidden" style={{ border: "1px solid var(--bb-border)" }}> |
|
||||||
<CeoBlock /> |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Анатомия */} |
|
||||||
<section className="space-y-4"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Структура блока |
|
||||||
</h2> |
|
||||||
<div className="space-y-3"> |
|
||||||
{[ |
|
||||||
{ |
|
||||||
num: "1", |
|
||||||
title: "Вводный абзац", |
|
||||||
bg: "#f0f9ff", |
|
||||||
desc: 'Специализация клиники, адреса. Ключевые слова — ссылки синего цвета (#0089c3).', |
|
||||||
}, |
|
||||||
{ |
|
||||||
num: "2", |
|
||||||
title: "Вопросы-стимулы", |
|
||||||
bg: "#fefce8", |
|
||||||
desc: 'Каждый вопрос — отдельный абзац, начинается с «—». Адресован пациенту с конкретным симптомом.', |
|
||||||
}, |
|
||||||
{ |
|
||||||
num: "3", |
|
||||||
title: "Заключительный абзац", |
|
||||||
bg: "#f0fdf4", |
|
||||||
desc: 'Призыв обращаться. «Будьте здоровы!» — фирменная подпись клиники.', |
|
||||||
}, |
|
||||||
].map((s) => ( |
|
||||||
<div |
|
||||||
key={s.num} |
|
||||||
className="flex gap-4 p-4 rounded-xl" |
|
||||||
style={{ background: s.bg, border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="w-7 h-7 rounded-full flex items-center justify-center font-bold text-sm shrink-0" |
|
||||||
style={{ background: "var(--brand-053m)", color: "#fff" }} |
|
||||||
> |
|
||||||
{s.num} |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
<p className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
|
||||||
{s.title} |
|
||||||
</p> |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{s.desc} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Вопросы-стимулы */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Вопросы-стимулы (5 штук) |
|
||||||
</h2> |
|
||||||
<div className="space-y-2"> |
|
||||||
{CEO_QUESTIONS.map((q, i) => ( |
|
||||||
<div |
|
||||||
key={i} |
|
||||||
className="flex items-start gap-3 p-3 rounded-lg text-sm" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<span className="font-bold shrink-0 mt-0.5" style={{ color: "var(--brand-053m)" }}> |
|
||||||
— |
|
||||||
</span> |
|
||||||
<span style={{ color: "var(--bb-text)" }}>{q}</span> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/ceo" version="v1.0" specText={LLM_CEO_TEXT}> |
|
||||||
<LlmSection title="Структура блока" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Часть", "Содержимое", "Примечание"]} |
|
||||||
rows={[ |
|
||||||
["Вводный абзац", "Специализация клиники, адреса", "Ключевые слова = ссылки #0089c3"], |
|
||||||
["Вопросы-стимулы", "5 вопросов от лица пациента, начинаются с «—»", "Без маркированных списков"], |
|
||||||
["Заключение", 'Призыв обращаться + «Будьте здоровы!»', "Фирменная подпись клиники"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Типографика" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Параметр", "Значение"]} |
|
||||||
rows={[ |
|
||||||
["Шрифт", "Fira Sans"], |
|
||||||
["Размер", "14px"], |
|
||||||
["Line-height", "1.6–1.8"], |
|
||||||
["Цвет текста", "#374151 (--bb-text)"], |
|
||||||
["Цвет ссылок в тексте", "#0089c3 (--brand-053m)"], |
|
||||||
["Фон блока", "#ffffff"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "Вопросы начинаются с «—» (длинное тире)" }, |
|
||||||
{ ok: true, text: "Ключевые медицинские термины — ссылки #0089c3" }, |
|
||||||
{ ok: true, text: "Фирменная подпись «Будьте здоровы!» в конце" }, |
|
||||||
{ ok: false, text: "Не добавлять маркированные списки (•)" }, |
|
||||||
{ ok: false, text: "Не добавлять H2 заголовок внутри блока" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,165 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; |
|
||||||
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const LLM_FORMS_TEXT = ` |
|
||||||
БЛОК: Формы записи и обратной связи |
|
||||||
Источник: perm.oclinica.ru/lor — два блока форм на одной странице |
|
||||||
Версия: v1.1 |
|
||||||
|
|
||||||
ФОРМА 1: «Будьте здоровы!» (форма записи на приём) |
|
||||||
Позиция: после блока отзывов |
|
||||||
Заголовок H2: «Будьте здоровы!» — 36px, bold, #000000, line-height 38px |
|
||||||
Подзаголовок: «Запишитесь на приём к врачу!» |
|
||||||
Фон секции: #d4f6f8 (светло-бирюзовый) |
|
||||||
Поля формы: |
|
||||||
1. Текстовый input: placeholder «Введите ваше имя» (height 50px) |
|
||||||
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px) |
|
||||||
3. Select: «Выберите ЛОР врача» (height 50px) |
|
||||||
4. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных» |
|
||||||
Кнопка: «Запишите меня!» — стиль bb-btn-primary (#FFA39C) |
|
||||||
Ширина формы: ~400px центрирована или в колонку |
|
||||||
|
|
||||||
ФОРМА 2: «Узнайте стоимость операции» (консультация) |
|
||||||
Позиция: после блока новостей |
|
||||||
Заголовок H2: «Узнайте стоимость операции» — 36px, bold, #000000, line-height 38px |
|
||||||
Подзаголовок: «Проконсультируйтесь с ассистентом хирурга» |
|
||||||
Фон секции: #d4f6f8 (тот же что и форма 1) |
|
||||||
Поля формы: |
|
||||||
1. Текстовый input: placeholder «каша» / «Введите ваше имя» (height 50px) |
|
||||||
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px) |
|
||||||
3. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных» |
|
||||||
Кнопка: «Перезвоните мне» — стиль bb-btn-primary (#FFA39C) |
|
||||||
|
|
||||||
ОБЩИЕ ПРАВИЛА: |
|
||||||
— Оба поля input/select: bb-input / bb-select (height 50px, border 1px solid #ccc) |
|
||||||
— Чекбокс обязателен в обеих формах |
|
||||||
— Кнопка отправки: всегда bb-btn-primary (#FFA39C) |
|
||||||
— Обе формы на бирюзовом фоне (#d4f6f8) |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ Чекбокс согласия обязателен в каждой форме |
|
||||||
✓ Кнопки отправки: bb-btn-primary (#FFA39C) |
|
||||||
✓ Обе формы на фоне #d4f6f8 |
|
||||||
✕ Не менять порядок полей |
|
||||||
✕ Не убирать чекбокс согласия |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [ |
|
||||||
{ |
|
||||||
version: "v1.1", |
|
||||||
date: "24.03.2026", |
|
||||||
changes: [ |
|
||||||
"H2: размер на 36px, цвет на #000000, line-height 38px", |
|
||||||
"Фон формы 1: с #b8e6ed на #d4f6f8", |
|
||||||
"Фон формы 2: с #ffffff на #d4f6f8 (обе формы на одном фоне)", |
|
||||||
], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export default function ContactFormsPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Формы записи |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/contact-forms" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<ContactFormsBlock /> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Сравнение двух форм */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Сравнение двух форм |
|
||||||
</h2> |
|
||||||
<div className="overflow-x-auto rounded-xl" style={{ border: "1px solid var(--bb-border)" }}> |
|
||||||
<table className="w-full text-sm border-collapse"> |
|
||||||
<thead> |
|
||||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}> |
|
||||||
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Параметр</th> |
|
||||||
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Форма 1 «Будьте здоровы!»</th> |
|
||||||
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Форма 2 «Стоимость операции»</th> |
|
||||||
</tr> |
|
||||||
</thead> |
|
||||||
<tbody> |
|
||||||
{[ |
|
||||||
["Заголовок", "«Будьте здоровы!»", "«Узнайте стоимость операции»"], |
|
||||||
["Подзаголовок", "«Запишитесь на приём к врачу!»", "«Проконсультируйтесь с ассистентом хирурга»"], |
|
||||||
["Фон секции", "#d4f6f8", "#d4f6f8"], |
|
||||||
["Поля", "Имя + Телефон + Select врача + Checkbox", "Имя + Телефон + Checkbox"], |
|
||||||
["Кнопка", "«Запишите меня!»", "«Перезвоните мне»"], |
|
||||||
["Стиль кнопки", "bb-btn-primary (#FFA39C)", "bb-btn-primary (#FFA39C)"], |
|
||||||
].map(([param, f1, f2]) => ( |
|
||||||
<tr key={param} style={{ borderTop: "1px solid var(--bb-border)" }}> |
|
||||||
<td className="px-4 py-2 font-medium text-xs" style={{ color: "var(--bb-text)" }}>{param}</td> |
|
||||||
<td className="px-4 py-2 text-xs" style={{ color: "var(--bb-text-muted)" }}>{f1}</td> |
|
||||||
<td className="px-4 py-2 text-xs" style={{ color: "var(--bb-text-muted)" }}>{f2}</td> |
|
||||||
</tr> |
|
||||||
))} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/contact-forms" version="v1.1" specText={LLM_FORMS_TEXT}> |
|
||||||
<LlmSection title="Форма 1 — Запись на приём" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Поле", "Тип", "Placeholder"]} |
|
||||||
rows={[ |
|
||||||
["Имя", "input[type=text]", "«Введите ваше имя»"], |
|
||||||
["Телефон", "input[type=tel]", "«Введите ваш телефон»"], |
|
||||||
["Врач", "select", "«Выберите ЛОР врача»"], |
|
||||||
["Согласие", "checkbox", "«Отправляя данные, я даю согласие...»"], |
|
||||||
["Кнопка", "button bb-btn-primary", "«Запишите меня!»"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Форма 2 — Запрос стоимости" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Поле", "Тип", "Placeholder"]} |
|
||||||
rows={[ |
|
||||||
["Имя", "input[type=text]", "«Введите ваше имя»"], |
|
||||||
["Телефон", "input[type=tel]", "«Введите ваш телефон»"], |
|
||||||
["Согласие", "checkbox", "«Отправляя данные, я даю согласие...»"], |
|
||||||
["Кнопка", "button bb-btn-primary", "«Перезвоните мне»"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "Обе формы: фон #d4f6f8" }, |
|
||||||
{ ok: true, text: "Чекбокс согласия обязателен в каждой форме" }, |
|
||||||
{ ok: true, text: "Кнопки: bb-btn-primary (#FFA39C)" }, |
|
||||||
{ ok: true, text: "Все input/select: height 50px (bb-input, bb-select)" }, |
|
||||||
{ ok: false, text: "Не убирать чекбокс согласия на обработку данных" }, |
|
||||||
{ ok: false, text: "Не менять стиль кнопки на outline или teal" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,170 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { FooterBlock } from "@/components/blocks/FooterBlock"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Подвал (Footer). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const FOOTER_COLUMNS = [ |
|
||||||
{ |
|
||||||
title: "О клинике", |
|
||||||
links: ["Лицензия", "Миссия", "Врачи", "Вакансии", "История", "Образовательная деятельность", "При инфо"], |
|
||||||
}, |
|
||||||
{ |
|
||||||
title: "Заболевания", |
|
||||||
links: ["Ринит", "Отит", "Гайморит", "Тонзиллит", "Полипы носа", "Искривление перегородки"], |
|
||||||
}, |
|
||||||
{ |
|
||||||
title: "Вопрос-ответ", |
|
||||||
links: [ |
|
||||||
"Что нужно знать до операции на ухо", |
|
||||||
"Что нужно знать до операции на нос", |
|
||||||
"Отзывы до и после лечения у детей", |
|
||||||
"Что нужно знать при лечении у детей", |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
title: "Операции", |
|
||||||
links: ["Септопластика", "Турбинопластика", "Тонзиллэктомия", "Аденотомия", "Тимпанопластика", "Мирингопластика"], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const LLM_FOOTER_TEXT = ` |
|
||||||
БЛОК: Подвал сайта (Footer) |
|
||||||
Источник: perm.oclinica.ru/lor — нижняя часть страницы |
|
||||||
Версия: v1.0 |
|
||||||
|
|
||||||
СТРУКТУРА ПОДВАЛА: |
|
||||||
|
|
||||||
1. ОСНОВНАЯ ЧАСТЬ — 4 колонки ссылок: |
|
||||||
Колонка 1 «О клинике»: Лицензия, Миссия, Врачи, Вакансии, История, Образовательная деятельность |
|
||||||
Колонка 2 «Заболевания»: Ринит, Отит, Гайморит, Тонзиллит, Полипы носа, Искривление перегородки |
|
||||||
Колонка 3 «Вопрос-ответ»: 4 вопроса об операциях и лечении |
|
||||||
Колонка 4 «Операции»: Септопластика, Турбинопластика, Тонзиллэктомия, Аденотомия и др. |
|
||||||
|
|
||||||
2. НИЖНЯЯ ПОЛОСА: |
|
||||||
Левая: Логотип «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н.ОЛЕНЕВОЙ» |
|
||||||
Центр: |
|
||||||
— «Мы находимся по адресу: Пермь, ул. Г. Звезда...» |
|
||||||
— Иконки соцсетей: VK, OK, YouTube, Telegram и другие |
|
||||||
Правая: Часы работы: |
|
||||||
— Пн-пт: 9:00–21:00 |
|
||||||
— Сб: 9:00–18:00 |
|
||||||
— Вс: выходной |
|
||||||
— Вторая клиника часы отдельно |
|
||||||
|
|
||||||
ЦВЕТА: |
|
||||||
Фон подвала: #fff или светло-серый (#f8f9fa) |
|
||||||
Заголовки колонок: #111827, font-weight 600 |
|
||||||
Ссылки: #53514e (--brand-073m), hover: #0089c3 |
|
||||||
Разделитель: border-top 1px solid #e5e7eb |
|
||||||
Часы работы: #374151 |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ 4 колонки ссылок в основной части |
|
||||||
✓ Логотип в нижней полосе слева |
|
||||||
✓ Адрес + соцсети в центре нижней полосы |
|
||||||
✓ Часы работы справа |
|
||||||
✕ Не менять структуру 4 колонок |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
export default function ContactFooterPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Подвал (Footer) |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/contact" defaultVersion="v1.0" defaultIsInPreview={false} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Подвал сайта с perm.oclinica.ru — 4 колонки ссылок, логотип, адрес, часы работы, соцсети. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<FooterBlock /> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Колонки */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Четыре колонки ссылок |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-4 gap-3"> |
|
||||||
{FOOTER_COLUMNS.map((col) => ( |
|
||||||
<div |
|
||||||
key={col.title} |
|
||||||
className="rounded-xl p-3 space-y-1" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<p className="font-semibold text-xs" style={{ color: "var(--bb-text)" }}> |
|
||||||
{col.title} |
|
||||||
</p> |
|
||||||
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{col.links.length} ссылок |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/contact" version="v1.0" specText={LLM_FOOTER_TEXT}> |
|
||||||
<LlmSection title="Структура подвала" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Зона", "Содержимое", "Фон"]} |
|
||||||
rows={[ |
|
||||||
["4 колонки ссылок", "О клинике / Заболевания / Вопрос-ответ / Операции", "#f8f9fa"], |
|
||||||
["Нижняя полоса — лево", "Логотип клиники (иконка + текст)", "#fff"], |
|
||||||
["Нижняя полоса — центр", "Адрес + иконки соцсетей (VK, OK, YT, TG)", "#fff"], |
|
||||||
["Нижняя полоса — право", "Часы работы Пн–пт / Сб / Вс", "#fff"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Часы работы" /> |
|
||||||
<LlmTable |
|
||||||
headers={["День", "Часы"]} |
|
||||||
rows={[ |
|
||||||
["Пн–пт", "9:00–21:00"], |
|
||||||
["Сб", "9:00–18:00"], |
|
||||||
["Вс", "выходной"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Цвета" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Цвет", "Токен"]} |
|
||||||
rows={[ |
|
||||||
["Заголовки колонок", "#111827, font-weight 600", "—"], |
|
||||||
["Ссылки (default)", "#53514e", "--brand-073m"], |
|
||||||
["Ссылки (hover)", "#0089c3", "--brand-053m"], |
|
||||||
["Фон основной части", "#f8f9fa", "—"], |
|
||||||
["Фон нижней полосы", "#ffffff", "—"], |
|
||||||
["Разделитель", "1px solid #e5e7eb", "—"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "4 колонки: О клинике / Заболевания / Вопрос-ответ / Операции" }, |
|
||||||
{ ok: true, text: "Логотип в нижней полосе слева" }, |
|
||||||
{ ok: true, text: "Адрес и соцсети в центре" }, |
|
||||||
{ ok: true, text: "Часы работы справа" }, |
|
||||||
{ ok: false, text: "Не менять структуру и порядок 4 колонок" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,168 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const LLM_DOCTORS_TEXT = ` |
|
||||||
БЛОК: Наши врачи |
|
||||||
Источник: perm.oclinica.ru/lor — блок под CEO-текстом |
|
||||||
Версия: v1.2 |
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА: |
|
||||||
1. ЗАГОЛОВОК H2: «Приём ведут опытные ЛОР врачи» |
|
||||||
Подзаголовок: описание принципа работы врачей клиники |
|
||||||
Размер: 36px, font-bold, #000000, line-height: 38px |
|
||||||
|
|
||||||
2. БЛОК СТАТИСТИКИ (3 показателя в ряд): |
|
||||||
— «Ежедневно 27 ЛОР врачей работают в клинике» |
|
||||||
— «В том числе 6 кандидатов медицинских наук» |
|
||||||
— «Свыше 12 000 успешно проведённых операций» |
|
||||||
Стиль: весь текст #60959c, bold, ~18px. Фона НЕТ. Снизу border-bottom 3px solid #60959c |
|
||||||
|
|
||||||
3. СЕТКА КАРТОЧЕК ВРАЧЕЙ (6 штук, 6 в ряд): |
|
||||||
Каждая карточка: |
|
||||||
— Фото врача 110×150px, object-fit: cover, object-position: center top, border-radius 4px |
|
||||||
— Имя (12px, font-weight 500, цвет #60959c) |
|
||||||
— Специализация (11px, #374151) |
|
||||||
Карточки без рамки, gap минимальный |
|
||||||
|
|
||||||
ЦВЕТА: |
|
||||||
Заголовок H2: #000000, 36px, line-height 38px |
|
||||||
Статистика текст: #60959c (серо-бирюзовый) |
|
||||||
Статистика черта: border-bottom 3px solid #60959c |
|
||||||
Имя врача: #60959c |
|
||||||
Специализация: #374151 |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ Заголовок H2 + описание обязательны |
|
||||||
✓ 3 stat-блока в ряд, без фоновых блоков |
|
||||||
✓ Сетка 6 колонок (6 карточек в ряд) |
|
||||||
✕ Не отображать более 6 врачей в основном блоке |
|
||||||
✕ Не убирать статистику |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [ |
|
||||||
{ |
|
||||||
version: "v1.2", |
|
||||||
date: "24.03.2026", |
|
||||||
changes: [ |
|
||||||
"H2: размер с ~30px на 36px, цвет с #111827 на #000000", |
|
||||||
"H2 line-height: 38px", |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
version: "v1.1", |
|
||||||
date: "23.03.2026", |
|
||||||
changes: [ |
|
||||||
"6 реальных фото врачей с сайта", |
|
||||||
"Статистика без фона, только border-bottom #60959c", |
|
||||||
], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export default function DoctorsBlockPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Блок «Наши врачи» |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/doctors" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-4"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<div |
|
||||||
className="rounded-xl p-8" |
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<DoctorsBlock /> |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Стат-блоки — разбор */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Блок статистики |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-3 gap-6"> |
|
||||||
{STATS.map((s) => ( |
|
||||||
<div |
|
||||||
key={s.num} |
|
||||||
className="pb-3 space-y-1" |
|
||||||
style={{ borderBottom: "3px solid #60959c" }} |
|
||||||
> |
|
||||||
<p className="text-base font-bold leading-snug" style={{ color: "#60959c" }}> |
|
||||||
{s.prefix} {s.num} {s.label} |
|
||||||
</p> |
|
||||||
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
цвет текста: #60959c · черта: 3px solid #60959c |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/doctors" version="v1.2" specText={LLM_DOCTORS_TEXT}> |
|
||||||
<LlmSection title="Структура блока" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Содержимое", "Стиль"]} |
|
||||||
rows={[ |
|
||||||
["H2", "«Приём ведут опытные ЛОР врачи»", "36px, font-bold, #000000, line-height 38px"], |
|
||||||
["Подзаголовок", "Описание принципа работы", "14px, #374151, line-height 1.7"], |
|
||||||
["Статистика", "3 блока в ряд, без фона", "#60959c bold + border-bottom 3px solid #60959c"], |
|
||||||
["Сетка врачей", "6 карточек в 1 ряд", "фото 110×150px + имя #60959c + специализация #374151"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Три показателя" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Число", "Описание"]} |
|
||||||
rows={STATS.map((s) => [`${s.prefix} ${s.num}`, s.label])} |
|
||||||
/> |
|
||||||
<LlmSection title="6 врачей слайдера (perm.oclinica.ru/lor)" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Имя", "Специализация"]} |
|
||||||
rows={DOCTORS.map((d) => [d.name, d.spec])} |
|
||||||
/> |
|
||||||
<LlmSection title="Карточка врача" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Поле", "Размер / Стиль"]} |
|
||||||
rows={[ |
|
||||||
["Фото", "110×150px, object-fit: cover, object-position: center top, border-radius 4px"], |
|
||||||
["Имя", "12px, font-weight 500, #60959c"], |
|
||||||
["Специализация", "11px, #374151"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "H2 + описание обязательны" }, |
|
||||||
{ ok: true, text: "3 stat-блока в ряд, без фоновых рамок" }, |
|
||||||
{ ok: true, text: "Сетка 6 карточек в 1 ряд (6 колонок)" }, |
|
||||||
{ ok: false, text: "Не показывать более 6 врачей в основном блоке" }, |
|
||||||
{ ok: false, text: "Не убирать статистику" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,206 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const LLM_HERO_TEXT = ` |
|
||||||
БЛОК: Hero-баннер (главный баннер страницы) |
|
||||||
Источник: perm.oclinica.ru/lor — реальный баннер раздела ЛОР |
|
||||||
Версия: v1.2 |
|
||||||
|
|
||||||
ЗАГОЛОВОК СТРАНИЦЫ (H1, над баннером): |
|
||||||
«ЛОР Клиника ухо, горло, нос - медицинский центр лечения ЛОР заболеваний у детей и взрослых» |
|
||||||
Шрифт: Fira Sans, 36px, weight 700, цвет: #cb9768 |
|
||||||
|
|
||||||
СТРУКТУРА БАННЕРА (двухколоночная, единый фон #f9f4e7): |
|
||||||
Левая колонка (~50%): |
|
||||||
— Заголовок: «ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ ЛЕЧЕНИЕ ЛОР ОРГАНОВ» |
|
||||||
Шрифт: Fira Sans, 22px, weight 700, uppercase, цвет #333333 |
|
||||||
— 3 пункта с галочками (✓ бежевый #bf9975): |
|
||||||
1. «БЕЗОПАСНО – оперируют хирурги с 15-летним опытом работы» |
|
||||||
2. «БЕЗ ВНЕШНИХ РАЗРЕЗОВ – хирургия сверхмалых размеров» |
|
||||||
3. «БЫСТРО – под наблюдением врача пациент находится 1 сутки» |
|
||||||
Ключевое слово: uppercase bold; описание: обычный текст, ~13px |
|
||||||
— Кнопка «Узнать стоимость операции» — стиль: bb-btn-pill (кремовый фон #E9E4D4, radius 25px) |
|
||||||
Правая колонка (~50%): |
|
||||||
— Фото врача с пациентом |
|
||||||
— Изображение занимает всю высоту блока |
|
||||||
|
|
||||||
ПОД БАННЕРОМ: |
|
||||||
— Кнопки соцсетей (VK, FB, TW), цвет #9ca3af |
|
||||||
— Счётчик просмотров |
|
||||||
|
|
||||||
ЦВЕТА: |
|
||||||
Фон баннера: #f9f4e7 (светло-кремовый, единый для всего блока) |
|
||||||
Кнопка CTA: pill-стиль (кремовый #E9E4D4, border #D5CFBD, radius 25px) |
|
||||||
Заголовок блока: #333333 |
|
||||||
Пункты: ключевое слово #111827 bold, описание #374151 |
|
||||||
Галочка: #bf9975 (бежевый) |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ Фон баннера всегда #f9f4e7 (светло-кремовый) — единый, без разделения на зоны |
|
||||||
✓ Заголовок блока uppercase, жирный |
|
||||||
✓ Три пункта с галочками обязательны |
|
||||||
✕ Не менять фон баннера на другой цвет |
|
||||||
✕ Не разбивать фон на два разных цвета |
|
||||||
✕ Не убирать три пункта с галочками |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [ |
|
||||||
{ |
|
||||||
version: "v1.2", |
|
||||||
date: "24.03.2026", |
|
||||||
changes: [ |
|
||||||
"H1: цвет исправлен с #53514e на #cb9768, размер с ~20px на 36px", |
|
||||||
"Заголовок баннера: размер с ~16px на 22px, цвет с #111827 на #333333", |
|
||||||
"CTA-кнопка: стиль изменён с outline на pill (фон #E9E4D4, radius 25px)", |
|
||||||
"Дефис в H1: длинное тире «–» заменено на простой дефис «-» (как на сайте)", |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
version: "v1.1", |
|
||||||
date: "23.03.2026", |
|
||||||
changes: [ |
|
||||||
"Единый фон #f9f4e7 (ранее был разбит на две зоны)", |
|
||||||
"Реальное фото врача с пациентом", |
|
||||||
], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export default function HeroPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Hero-баннер |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/hero" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "} |
|
||||||
<strong>#f9f4e7</strong>. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<HeroBlock /> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Анатомия */} |
|
||||||
<section className="space-y-4"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Анатомия баннера |
|
||||||
</h2> |
|
||||||
<div |
|
||||||
className="rounded-xl p-5 space-y-2" |
|
||||||
style={{ background: "#f5f0e8", border: "1px solid #d5cfbd" }} |
|
||||||
> |
|
||||||
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}> |
|
||||||
Весь баннер — единый фон <span className="font-mono">#f9f4e7</span> |
|
||||||
</p> |
|
||||||
<ul className="space-y-1"> |
|
||||||
{[ |
|
||||||
"Фон: #f9f4e7 (светло-кремовый) — одинаковый для всего блока", |
|
||||||
"Левая половина (~50%): заголовок uppercase bold + 3 галочки + кнопка outline", |
|
||||||
"Правая половина (~50%): фото врача с пациентом", |
|
||||||
"Минимальная высота: ~280px", |
|
||||||
].map((item) => ( |
|
||||||
<li key={item} className="text-xs flex items-start gap-1.5"> |
|
||||||
<span style={{ color: "var(--brand-053m)" }}>·</span> |
|
||||||
<span style={{ color: "var(--bb-text-muted)" }}>{item}</span> |
|
||||||
</li> |
|
||||||
))} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Три пункта с галочками */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Три пункта баннера |
|
||||||
</h2> |
|
||||||
<div className="space-y-2"> |
|
||||||
{HERO_CHECKS.map((c) => ( |
|
||||||
<div |
|
||||||
key={c.key} |
|
||||||
className="flex items-start gap-3 p-3 rounded-lg" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<span className="font-bold text-lg shrink-0" style={{ color: "#bf9975" }}> |
|
||||||
✓ |
|
||||||
</span> |
|
||||||
<div> |
|
||||||
<span className="text-sm font-bold uppercase" style={{ color: "#111827" }}> |
|
||||||
{c.key} |
|
||||||
</span> |
|
||||||
<span className="text-sm" style={{ color: "#374151" }}> |
|
||||||
{" "}– {c.desc} |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Ключевое слово: uppercase + bold. Описание: обычный текст. Галочка: #bf9975 (бежевый). |
|
||||||
</p> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/hero" version="v1.2" specText={LLM_HERO_TEXT}> |
|
||||||
<LlmSection title="Структура баннера" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Зона", "Ширина", "Фон", "Содержимое"]} |
|
||||||
rows={[ |
|
||||||
["Весь баннер", "100%", "#f9f4e7", "Единый светло-кремовый фон"], |
|
||||||
["Левый блок", "~50%", "#f9f4e7 (общий)", "Заголовок uppercase + 3 галочки + кнопка pill"], |
|
||||||
["Правый блок", "~50%", "#f9f4e7 (общий)", "Фото врача с пациентом"], |
|
||||||
["Под баннером", "100%", "#fff", "Кнопки соцсетей + счётчик просмотров"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Три пункта баннера" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Ключевое слово", "Описание"]} |
|
||||||
rows={HERO_CHECKS.map((c) => [c.key, c.desc])} |
|
||||||
/> |
|
||||||
<LlmSection title="Цвета" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Цвет", "Токен"]} |
|
||||||
rows={[ |
|
||||||
["Фон баннера (единый)", "#f9f4e7", "Светло-кремовый фон"], |
|
||||||
["Кнопка CTA", "pill-стиль (#E9E4D4, 25px)", "bb-btn-pill"], |
|
||||||
["Заголовок блока", "#333333", "—"], |
|
||||||
["H1 страницы", "#cb9768", "36px, bold"], |
|
||||||
["Галочка ✓", "#bf9975", "Бежевый"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "Фон баннера: #f9f4e7 (светло-кремовый) — единый для всего блока" }, |
|
||||||
{ ok: true, text: "Кнопка CTA: bb-btn-pill (кремовый #E9E4D4, radius 25px)" }, |
|
||||||
{ ok: true, text: "Заголовок: uppercase, bold" }, |
|
||||||
{ ok: true, text: "Три пункта с галочками ✓ (#bf9975)" }, |
|
||||||
{ ok: false, text: "Не менять фон баннера на другой цвет" }, |
|
||||||
{ ok: false, text: "Не разбивать баннер на два разных цвета фона" }, |
|
||||||
{ ok: false, text: "Не убирать три пункта с галочками" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,227 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { NavigationBlock, NAV_ITEMS } from "@/components/blocks/NavigationBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const LLM_NAV_TEXT = ` |
|
||||||
БЛОК: Шапка и навигация сайта (Header) |
|
||||||
Источник: perm.oclinica.ru/lor — реальная шапка сайта |
|
||||||
Версия: v1.0 |
|
||||||
|
|
||||||
СТРУКТУРА ШАПКИ (3 зоны сверху вниз): |
|
||||||
|
|
||||||
1. ТОП-БАР (~40px высота, фон #fff, border-bottom 1px solid #e5e7eb) |
|
||||||
Левая часть: |
|
||||||
— Адрес: «Б. Цитная, 3» с иконкой 📍 |
|
||||||
— Ссылка: «Клиника ухо горло нос и аллергия» |
|
||||||
— Ссылка: «Центр аллергологии и пульмонологии» |
|
||||||
Правая часть: |
|
||||||
— Телефон: «/342/ 255-53-84» (font-weight: bold, color: #111827) |
|
||||||
— Кнопка «Заказать звонок» — стиль bb-btn-pill (#e9e4d4, border #d5cfbd, border-radius 25px) |
|
||||||
|
|
||||||
2. ЛОГОТИП (~64px, фон #fff) |
|
||||||
— Иконка кружок синий (#0089c3) с крестом |
|
||||||
— Текст «КЛИНИКА УХО ГОРЛО НОС» жирный uppercase, цвет #53514e |
|
||||||
— Подпись «им. проф. Е.Н.Оленевой» мелкий, цвет #9ca3af |
|
||||||
|
|
||||||
3. ГЛАВНОЕ МЕНЮ (~46px, фон #fff, border-top 1px solid #e5e7eb) |
|
||||||
Пункты (8 штук): Клиника | ЛОР врачи | Заболевания | Вопрос-ответ | ЛОР-операции | Сурдология | Цены | Контакты |
|
||||||
Шрифт: Fira Sans 14px, weight 400 |
|
||||||
Цвет ссылок: #53514e (--brand-073m) |
|
||||||
Активный / Hover: #0089c3 (--brand-053m) |
|
||||||
Разделители: border-right 1px solid #f3f4f6 между пунктами |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ Логотип всегда кликабелен — ведёт на главную страницу раздела |
|
||||||
✓ Кнопка «Заказать звонок» всегда видна, pill-стиль |
|
||||||
✓ Телефон кликабелен (tel: ссылка) |
|
||||||
✓ Активный пункт меню — цвет #0089c3, остальные #53514e |
|
||||||
✕ Не добавлять пункты меню, которых нет на сайте |
|
||||||
✕ Не менять порядок пунктов меню |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
export default function NavigationPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Шапка и навигация |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/navigation" defaultVersion="v1.0" defaultIsInPreview={true} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Точное воспроизведение шапки с perm.oclinica.ru/lor. Три зоны: топ-бар, логотип, главное меню. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<div className="rounded-xl overflow-hidden"> |
|
||||||
<NavigationBlock /> |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Анатомия */} |
|
||||||
<section className="space-y-4"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Анатомия шапки |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
||||||
{[ |
|
||||||
{ |
|
||||||
zone: "1. Топ-бар", |
|
||||||
bg: "#f0f9ff", |
|
||||||
height: "~40px", |
|
||||||
color: "Фон: #fff", |
|
||||||
desc: "Адрес, ссылки на разделы клиники, телефон, кнопка «Заказать звонок» (pill)", |
|
||||||
}, |
|
||||||
{ |
|
||||||
zone: "2. Логотип", |
|
||||||
bg: "#f0fdf4", |
|
||||||
height: "~64px", |
|
||||||
color: "Фон: #fff", |
|
||||||
desc: "Иконка-кружок (#0089c3) + название клиники двумя строками + подпись мелким", |
|
||||||
}, |
|
||||||
{ |
|
||||||
zone: "3. Навигация", |
|
||||||
bg: "#fefce8", |
|
||||||
height: "~46px", |
|
||||||
color: "Фон: #fff, border-top", |
|
||||||
desc: "8 горизонтальных пунктов, активный = #0089c3, остальные = #53514e", |
|
||||||
}, |
|
||||||
].map((z) => ( |
|
||||||
<div |
|
||||||
key={z.zone} |
|
||||||
className="rounded-xl p-4 space-y-2" |
|
||||||
style={{ background: z.bg, border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}> |
|
||||||
{z.zone} |
|
||||||
</p> |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{z.desc} |
|
||||||
</p> |
|
||||||
<p className="text-[11px] font-mono mt-1" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
H: {z.height} · {z.color} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Пункты меню */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Пункты главного меню |
|
||||||
</h2> |
|
||||||
<div className="flex flex-wrap gap-2"> |
|
||||||
{NAV_ITEMS.map((item, i) => ( |
|
||||||
<span |
|
||||||
key={item} |
|
||||||
className="px-3 py-1.5 rounded text-sm" |
|
||||||
style={{ |
|
||||||
background: i === 0 ? "#dff0fa" : "var(--bb-sidebar-bg)", |
|
||||||
color: i === 0 ? "var(--brand-053m)" : "var(--bb-text)", |
|
||||||
border: "1px solid var(--bb-border)", |
|
||||||
fontWeight: i === 0 ? 500 : 400, |
|
||||||
}} |
|
||||||
> |
|
||||||
{item} |
|
||||||
{i === 0 && ( |
|
||||||
<span className="ml-1.5 text-[10px]" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
(активный) |
|
||||||
</span> |
|
||||||
)} |
|
||||||
</span> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Порядок пунктов фиксирован. Hover и активный пункт — цвет #0089c3 (--brand-053m). |
|
||||||
</p> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Токены */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Цвета и токены |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3"> |
|
||||||
{[ |
|
||||||
{ label: "Фон шапки", value: "#ffffff", token: "—" }, |
|
||||||
{ label: "Ссылки меню", value: "#53514e", token: "--brand-073m" }, |
|
||||||
{ label: "Активный / hover", value: "#0089c3", token: "--brand-053m" }, |
|
||||||
{ label: "Кнопка «Заказать»", value: "#e9e4d4", token: "bb-btn-pill" }, |
|
||||||
].map((t) => ( |
|
||||||
<div |
|
||||||
key={t.label} |
|
||||||
className="p-3 rounded-xl space-y-2" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="w-8 h-8 rounded border" |
|
||||||
style={{ background: t.value, borderColor: "var(--bb-border)" }} |
|
||||||
/> |
|
||||||
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}> |
|
||||||
{t.label} |
|
||||||
</p> |
|
||||||
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{t.value} |
|
||||||
</p> |
|
||||||
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{t.token} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/navigation" version="v1.0" specText={LLM_NAV_TEXT}> |
|
||||||
<LlmSection title="Зоны шапки" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Зона", "Высота", "Фон", "Содержимое"]} |
|
||||||
rows={[ |
|
||||||
["Топ-бар", "~40px", "#fff", "Адрес, ссылки разделов, телефон /342/ 255-53-84, кнопка «Заказать звонок»"], |
|
||||||
["Логотип", "~64px", "#fff", "Кружок #0089c3 + «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н.ОЛЕНЕВОЙ»"], |
|
||||||
["Навигация", "~46px", "#fff + border-top", "8 пунктов: Клиника / ЛОР врачи / Заболевания / Вопрос-ответ / ЛОР-операции / Сурдология / Цены / Контакты"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Цвета" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Цвет", "Токен"]} |
|
||||||
rows={[ |
|
||||||
["Фон шапки", "#ffffff", "—"], |
|
||||||
["Ссылки меню (default)", "#53514e", "--brand-073m"], |
|
||||||
["Активный пункт / hover", "#0089c3", "--brand-053m"], |
|
||||||
["Кнопка «Заказать звонок»", "#e9e4d4 / border #d5cfbd", "bb-btn-pill"], |
|
||||||
["Телефон", "#111827, font-weight bold", "—"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "Логотип ведёт на главную страницу раздела" }, |
|
||||||
{ ok: true, text: "Кнопка «Заказать звонок» всегда видна, pill-стиль" }, |
|
||||||
{ ok: true, text: "Телефон — кликабельная ссылка tel:" }, |
|
||||||
{ ok: true, text: "Активный пункт меню — цвет #0089c3" }, |
|
||||||
{ ok: false, text: "Не добавлять пункты меню, которых нет на сайте" }, |
|
||||||
{ ok: false, text: "Не менять порядок 8 пунктов меню" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,178 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; |
|
||||||
import { NewsBlock } from "@/components/blocks/NewsBlock"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Блок «Новости». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const MOCK_NEWS = [ |
|
||||||
{ |
|
||||||
date: "20.12.2025", |
|
||||||
title: "Наша работа клиники и новогодние праздники", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
{ |
|
||||||
date: "11.08.2025", |
|
||||||
title: "СЕРВИС ОБНОВЛЕНИЕ: Обновление графика работы клиники", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
{ |
|
||||||
date: "12.06.2025", |
|
||||||
title: "СЕРВИС ОБНОВЛЕНИЕ: Временное изменение работы клиники 22.06.25 г.", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
{ |
|
||||||
date: "11.06.2025", |
|
||||||
title: "График работы клиники в ближайшие праздники", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const LLM_NEWS_TEXT = ` |
|
||||||
БЛОК: Новости |
|
||||||
Источник: perm.oclinica.ru/lor — блок новостей внизу страницы |
|
||||||
Версия: v1.1 |
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА: |
|
||||||
1. ЗАГОЛОВОК H2: «Новости» |
|
||||||
Выравнивание: по центру или слева |
|
||||||
|
|
||||||
2. СЕТКА КАРТОЧЕК НОВОСТЕЙ (4 штуки в ряд): |
|
||||||
Каждая карточка: |
|
||||||
— Дата: малый текст сверху (формат DD.MM.YYYY), цвет #6b7280 |
|
||||||
— Заголовок новости: синяя ссылка (#0089c3), 14px, font-weight 500, hover underline |
|
||||||
— Без превью-изображения |
|
||||||
— Без описания (только дата + заголовок-ссылка) |
|
||||||
Hover на карточке: bg #eef4d1, box-shadow 0 0 16px 0 #9e9e9a (bb-news-card) |
|
||||||
|
|
||||||
3. КНОПКА «Все новости»: |
|
||||||
Стиль: outline (#BF9975) или teal (#60959c), выровнена по центру под сеткой |
|
||||||
На сайте: ссылка «Все новости» |
|
||||||
|
|
||||||
ЦВЕТА: |
|
||||||
Дата: #6b7280 |
|
||||||
Заголовок-ссылка: #0089c3 (--brand-053m) |
|
||||||
Hover фон карточки: #eef4d1 |
|
||||||
Hover тень: 0 0 16px 0 #9e9e9a |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ 4 карточки в ряд |
|
||||||
✓ Дата сверху карточки |
|
||||||
✓ Заголовок = ссылка #0089c3 |
|
||||||
✓ Hover: bg #eef4d1 (класс bb-news-card) |
|
||||||
✓ Кнопка «Все новости» под сеткой |
|
||||||
✕ Не добавлять изображения в карточки новостей главной страницы |
|
||||||
✕ Не добавлять описание/анонс в карточку |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [ |
|
||||||
{ |
|
||||||
version: "v1.1", |
|
||||||
date: "24.03.2026", |
|
||||||
changes: [ |
|
||||||
"H2: размер на 36px, цвет на #000000, line-height 38px", |
|
||||||
"Фон секции: с белого на #f2fee6 (светло-зелёный, как на реальном сайте)", |
|
||||||
], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export default function NewsBlockPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Блок «Новости» |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/news" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка), |
|
||||||
кнопка «Все новости». |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-4"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<NewsBlock /> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Анатомия карточки */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Анатомия карточки новости |
|
||||||
</h2> |
|
||||||
<div |
|
||||||
className="rounded-xl p-6 max-w-xs" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<div className="space-y-3"> |
|
||||||
<div |
|
||||||
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono" |
|
||||||
style={{ background: "#f0f9ff", color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
① Дата: DD.MM.YYYY · цвет #6b7280 |
|
||||||
</div> |
|
||||||
<div |
|
||||||
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono" |
|
||||||
style={{ background: "#f0f9ff", color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
② Заголовок-ссылка · #0089c3 · font-weight 500 |
|
||||||
</div> |
|
||||||
<div |
|
||||||
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono" |
|
||||||
style={{ background: "#fefce8", color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
Hover: bg #eef4d1 · shadow 0 0 16px #9e9e9a |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Нет изображений, нет описания — только дата и заголовок. |
|
||||||
CSS-класс <code className="font-mono">.bb-news-card</code> обрабатывает hover. |
|
||||||
</p> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/news" version="v1.1" specText={LLM_NEWS_TEXT}> |
|
||||||
<LlmSection title="Структура карточки новости" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Поле", "Стиль", "Значение"]} |
|
||||||
rows={[ |
|
||||||
["Дата", "text-xs, #6b7280", "Формат DD.MM.YYYY"], |
|
||||||
["Заголовок", "text-sm, font-weight 500, #0089c3", "Ссылка на новость"], |
|
||||||
["Hover фон", ".bb-news-card:hover", "#eef4d1"], |
|
||||||
["Hover тень", ".bb-news-card:hover", "0 0 16px 0 #9e9e9a"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Mock-данные (4 реальные новости с сайта)" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Дата", "Заголовок"]} |
|
||||||
rows={MOCK_NEWS.map((n) => [n.date, n.title])} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "4 карточки в ряд (grid-cols-4)" }, |
|
||||||
{ ok: true, text: "Дата сверху, заголовок-ссылка ниже" }, |
|
||||||
{ ok: true, text: "Hover: класс bb-news-card (bg #eef4d1)" }, |
|
||||||
{ ok: true, text: "Кнопка «Все новости» под сеткой (bb-btn-outline)" }, |
|
||||||
{ ok: false, text: "Не добавлять изображения в карточки" }, |
|
||||||
{ ok: false, text: "Не добавлять текст-анонс в карточку" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,209 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog"; |
|
||||||
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: "Блок «Отзывы». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
const MOCK_REVIEWS = [ |
|
||||||
{ |
|
||||||
text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.", |
|
||||||
author: "Пациент клиники", |
|
||||||
doctor: "Тимофеева Наталья Александровна", |
|
||||||
}, |
|
||||||
{ |
|
||||||
text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.", |
|
||||||
author: "Наталья К.", |
|
||||||
doctor: "Макарова Людмила Тимофеевна", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const LLM_REVIEWS_TEXT = ` |
|
||||||
БЛОК: Отзывы о нас |
|
||||||
Источник: perm.oclinica.ru/lor — блок отзывов |
|
||||||
Версия: v1.1 |
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА: |
|
||||||
1. ЗАГОЛОВОК H2: «Отзывы о нас» |
|
||||||
Подзаголовок: «За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа |
|
||||||
более 50000 пациентов. Но дня сейчас мы высоко ценим каждый положительный отзыв и искренние слова благодарности» |
|
||||||
|
|
||||||
2. КАРУСЕЛЬ ОТЗЫВОВ: |
|
||||||
— Большая открывающая кавычка (« «) в цвете #b8e6ed, font-size 80–100px |
|
||||||
— Текст отзыва: курсив, 15–16px, #374151, background #eef4d1 |
|
||||||
— Ссылка «Читать отзыв полностью» в цвете #0089c3 |
|
||||||
— Стрелки навигации < > по бокам (round buttons) |
|
||||||
— Карусель показывает 1 отзыв за раз |
|
||||||
|
|
||||||
ЦВЕТА: |
|
||||||
Фон карточки отзыва: #eef4d1 (светло-жёлто-зелёный) |
|
||||||
Кавычка: #b8e6ed (светло-бирюзовый) |
|
||||||
Текст отзыва: #374151 |
|
||||||
Ссылка «Читать полностью»: #0089c3 |
|
||||||
|
|
||||||
ПРАВИЛА: |
|
||||||
✓ Фон карточки отзыва: #eef4d1 (тот же что у ReviewCard) |
|
||||||
✓ Большая декоративная кавычка |
|
||||||
✓ Ссылка «Читать отзыв полностью» обязательна |
|
||||||
✓ Навигация карусели (стрелки) |
|
||||||
✕ Не показывать более 1 отзыва за раз в карусели |
|
||||||
✕ Не убирать навигационные стрелки |
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [ |
|
||||||
{ |
|
||||||
version: "v1.1", |
|
||||||
date: "24.03.2026", |
|
||||||
changes: [ |
|
||||||
"H2: размер с ~20px на 36px, цвет с #111827 на #000000, line-height 38px", |
|
||||||
], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export default function ReviewsBlockPage() { |
|
||||||
return ( |
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10"> |
|
||||||
{/* Заголовок страницы */} |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest mb-1" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Блоки |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
Блок «Отзывы о нас» |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/blocks/reviews" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} /> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Живой пример */} |
|
||||||
<section className="space-y-4"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Живой пример |
|
||||||
</h2> |
|
||||||
<ReviewsBlock /> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Несколько примеров */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Примеры отзывов (mock) |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
||||||
{MOCK_REVIEWS.map((r, i) => ( |
|
||||||
<div |
|
||||||
key={i} |
|
||||||
className="rounded-xl p-5 space-y-3" |
|
||||||
style={{ background: "#eef4d1", border: "1px solid #d4e6a0" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="text-5xl leading-none font-serif" |
|
||||||
style={{ color: "#b8e6ed" }} |
|
||||||
> |
|
||||||
« |
|
||||||
</div> |
|
||||||
<p |
|
||||||
className="text-sm italic leading-relaxed" |
|
||||||
style={{ color: "#374151" }} |
|
||||||
> |
|
||||||
{r.text} |
|
||||||
</p> |
|
||||||
<div className="pt-1"> |
|
||||||
<a href="#" className="text-sm" style={{ color: "#0089c3" }}> |
|
||||||
Читать отзыв полностью |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
<div className="border-t pt-2" style={{ borderColor: "#c8d8a0" }}> |
|
||||||
<p className="text-xs font-medium" style={{ color: "#374151" }}> |
|
||||||
{r.author} |
|
||||||
</p> |
|
||||||
<p className="text-[11px]" style={{ color: "#6b7280" }}> |
|
||||||
о враче: {r.doctor} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* Анатомия */} |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
Анатомия карточки отзыва |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3"> |
|
||||||
{[ |
|
||||||
{ label: "Фон карточки", value: "#eef4d1", note: "светло-жёлто-зелёный" }, |
|
||||||
{ label: "Кавычка декоративная", value: "#b8e6ed", note: "80–100px, font-serif" }, |
|
||||||
{ label: "Текст отзыва", value: "#374151", note: "14px, italic" }, |
|
||||||
{ label: "Ссылка", value: "#0089c3", note: "--brand-053m" }, |
|
||||||
].map((t) => ( |
|
||||||
<div |
|
||||||
key={t.label} |
|
||||||
className="p-3 rounded-xl space-y-1" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="w-8 h-8 rounded border" |
|
||||||
style={{ background: t.value, borderColor: "var(--bb-border)" }} |
|
||||||
/> |
|
||||||
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}> |
|
||||||
{t.label} |
|
||||||
</p> |
|
||||||
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{t.value} |
|
||||||
</p> |
|
||||||
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{t.note} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
|
|
||||||
{/* LLM блок */} |
|
||||||
<LlmBlock path="/blocks/reviews" version="v1.1" specText={LLM_REVIEWS_TEXT}> |
|
||||||
<LlmSection title="Структура блока" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Содержимое", "Стиль"]} |
|
||||||
rows={[ |
|
||||||
["H2", "«Отзывы о нас»", "36px, font-bold, #000000, line-height 38px"], |
|
||||||
["Подзаголовок", "Описание достижений клиники за 12 лет", "14px, #374151"], |
|
||||||
["Кавычка", "Декоративная «", "80–100px, #b8e6ed, font-serif"], |
|
||||||
["Текст отзыва", "Полный текст отзыва пациента", "14px, italic, #374151"], |
|
||||||
["Ссылка", "«Читать отзыв полностью»", "#0089c3"], |
|
||||||
["Стрелки карусели", "‹ ›", "Round buttons, фон var(--bb-sidebar-bg)"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Цвета" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Цвет", "Токен"]} |
|
||||||
rows={[ |
|
||||||
["Фон карточки отзыва", "#eef4d1", "—"], |
|
||||||
["Декоративная кавычка", "#b8e6ed", "—"], |
|
||||||
["Текст отзыва", "#374151", "—"], |
|
||||||
["Ссылка", "#0089c3", "--brand-053m"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "Фон карточки отзыва: #eef4d1" }, |
|
||||||
{ ok: true, text: "Декоративная кавычка цвет #b8e6ed" }, |
|
||||||
{ ok: true, text: "Ссылка «Читать отзыв полностью» обязательна" }, |
|
||||||
{ ok: true, text: "Навигация карусели: стрелки ‹ ›" }, |
|
||||||
{ ok: false, text: "Не показывать несколько отзывов одновременно" }, |
|
||||||
{ ok: false, text: "Не убирать навигационные стрелки" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,575 +0,0 @@ |
|||||||
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"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
|
|
||||||
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 VARIANTS = [ |
|
||||||
{ |
|
||||||
variant: "primary" as const, |
|
||||||
name: "Primary", |
|
||||||
label: "Запишитесь к нам", |
|
||||||
cssClass: ".bb-btn-primary", |
|
||||||
bg: "#FFA39C", |
|
||||||
border: "#FF847B", |
|
||||||
textColor: "#fff", |
|
||||||
radius: "7px", |
|
||||||
shadow: "да", |
|
||||||
where: "Кнопка отправки форм записи", |
|
||||||
example: "«Запишите меня!»", |
|
||||||
note: "Коралловый — самый заметный акцент на странице. Всегда один в форме.", |
|
||||||
}, |
|
||||||
{ |
|
||||||
variant: "outline" as const, |
|
||||||
name: "Outline", |
|
||||||
label: "Записаться на приём", |
|
||||||
cssClass: ".bb-btn-outline", |
|
||||||
bg: "#fff", |
|
||||||
border: "#BF9975", |
|
||||||
textColor: "#BF9975", |
|
||||||
radius: "7px", |
|
||||||
shadow: "нет", |
|
||||||
where: "Хедер, навигация, ссылки-кнопки", |
|
||||||
example: "«Записаться на прием», «Все новости»", |
|
||||||
note: "Бежевая рамка — ненавязчивый вторичный CTA. Не конкурирует с основной формой.", |
|
||||||
}, |
|
||||||
{ |
|
||||||
variant: "teal" as const, |
|
||||||
name: "Teal", |
|
||||||
label: "Позвонить", |
|
||||||
cssClass: ".bb-btn-teal", |
|
||||||
bg: "#60959c", |
|
||||||
border: "прозрачный", |
|
||||||
textColor: "#fff", |
|
||||||
radius: "7px", |
|
||||||
shadow: "нет", |
|
||||||
where: "Контактные действия — звонок", |
|
||||||
example: "«Позвонить»", |
|
||||||
note: "Серо-бирюзовый — цвет из реального CSS сайта. Близок к Oracal 066M.", |
|
||||||
}, |
|
||||||
{ |
|
||||||
variant: "pill" as const, |
|
||||||
name: "Pill", |
|
||||||
label: "Заказать звонок", |
|
||||||
cssClass: ".bb-btn-pill", |
|
||||||
bg: "#e9e4d4", |
|
||||||
border: "#d5cfbd", |
|
||||||
textColor: "#333", |
|
||||||
radius: "25px", |
|
||||||
shadow: "нет", |
|
||||||
where: "Модальные триггеры, мягкий CTA", |
|
||||||
example: "«Заказать звонок»", |
|
||||||
note: "Кремовый фон + pill-форма — мягкий стиль. Используется для открытия модальных окон.", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const LLM_BUTTONS_TEXT = `КНОПКИ — LLM-спецификация (с реального сайта oclinica.ru)
|
|
||||||
Версия: v2.0 · /components/buttons |
|
||||||
Источник CSS: perm.oclinica.ru/.../style.css |
|
||||||
|
|
||||||
ВАРИАНТЫ (реальный сайт) |
|
||||||
Вариант | CSS класс | Фон | Текст | Граница | Radius | Shadow | Применение |
|
||||||
primary | .bb-btn-primary | #FFA39C | #fff | #FF847B | 7px | да | Форм-сабмит «Запишите меня!» |
|
||||||
outline | .bb-btn-outline | #fff | #BF9975 | #BF9975 | 7px | нет | Хедер «Записаться на прием», ссылки-кнопки |
|
||||||
teal | .bb-btn-teal | #60959c | #fff | нет | 7px | нет | Звонок «Позвонить» |
|
||||||
pill | .bb-btn-pill | #e9e4d4 | #333 | #d5cfbd | 25px | нет | Callback «Заказать звонок» |
|
||||||
|
|
||||||
CSS С САЙТА (точные значения) |
|
||||||
/* форм-кнопка «Запишите меня!» */ |
|
||||||
button { background:#FFA39C; color:white; font-weight:bold; border:solid 1px #FF847B; |
|
||||||
height:42px; font-size:18px; box-shadow:0px 0px 5px rgba(0,0,0,0.5),0px 4px 5px rgba(0,0,0,0.3); } |
|
||||||
|
|
||||||
/* appointment — «Записаться на прием» */ |
|
||||||
.appointment { background:#FFF; border:#BF9975 solid 1px; color:#BF9975; |
|
||||||
font-size:14px; line-height:38px; padding:3px 12px; border-radius:7px; } |
|
||||||
|
|
||||||
/* show-phone — «Позвонить» */ |
|
||||||
.show-phone { background:rgb(96,149,156); color:#fff; border-radius:7px; |
|
||||||
font-size:14px; line-height:38px; padding:3px 12px; } |
|
||||||
|
|
||||||
/* callback — «Заказать звонок» */ |
|
||||||
a.callback_url { background:#e9e4d4; border:#d5cfbd solid 1px; color:#000; |
|
||||||
border-radius:25px; font-size:16px; padding:6px 18px; } |
|
||||||
|
|
||||||
РАЗМЕРЫ (брендбук-компонент) |
|
||||||
Размер | CSS класс | padding | font-size | Применение |
|
||||||
sm | .bb-btn-sm | 4px 11px | 13px | Компактные контексты |
|
||||||
md | .bb-btn-md | 8px 16px | 14px | Стандарт (appointment, teal, pill) |
|
||||||
lg | .bb-btn-lg | 10px 24px | 18px + bold | Форм-сабмит (соответствует реальному сайту) |
|
||||||
|
|
||||||
ПРАВИЛА ПРИМЕНЕНИЯ |
|
||||||
✓ primary (коралловый) — только для главного CTA в форме записи |
|
||||||
✓ outline (бежевый) — хедер, навигация, ссылки-кнопки на странице |
|
||||||
✓ teal (бирюзовый) — контактные действия (звонок, направление) |
|
||||||
✓ pill (кремовый) — открытие модальных окон, мягкий callback |
|
||||||
✓ Не более одного primary на форму |
|
||||||
✕ Не менять цвета вне фирменной палитры сайта |
|
||||||
✕ Primary — не для навигационных ссылок |
|
||||||
✕ Не накладывать тень на outline/teal/pill`.trim();
|
|
||||||
|
|
||||||
export default function ButtonsPage() { |
|
||||||
const codeHtml = `<!-- Primary — форм-кнопка «Запишите меня!» -->
|
|
||||||
<button class="bb-btn bb-btn-lg bb-btn-primary">Запишите меня!</button> |
|
||||||
|
|
||||||
<!-- Outline — appointment «Записаться на прием» --> |
|
||||||
<a class="bb-btn bb-btn-md bb-btn-outline" href="#form">Записаться на прием</a> |
|
||||||
|
|
||||||
<!-- Teal — «Позвонить» --> |
|
||||||
<a class="bb-btn bb-btn-md bb-btn-teal" href="tel:+73422250662">Позвонить</a> |
|
||||||
|
|
||||||
<!-- Pill — «Заказать звонок» --> |
|
||||||
<a class="bb-btn bb-btn-md bb-btn-pill" href="#callback">Заказать звонок</a>`;
|
|
||||||
|
|
||||||
const codeReact = `import { Button } from "@/components/ui/Button";
|
|
||||||
|
|
||||||
// Форм-кнопка (главный CTA)
|
|
||||||
<Button variant="primary" size="lg">Запишите меня!</Button> |
|
||||||
|
|
||||||
// Запись из хедера / навигации
|
|
||||||
<Button variant="outline" size="md">Записаться на прием</Button> |
|
||||||
|
|
||||||
// Звонок
|
|
||||||
<Button variant="teal" size="md">Позвонить</Button> |
|
||||||
|
|
||||||
// Заказать звонок (открывает модал)
|
|
||||||
<Button variant="pill" size="md">Заказать звонок</Button> |
|
||||||
|
|
||||||
// С loading-состоянием
|
|
||||||
<Button variant="primary" size="lg" loading>Отправляем...</Button>`;
|
|
||||||
|
|
||||||
const codeSiteExact = `/* Точный CSS с сайта oclinica.ru (style.css) */
|
|
||||||
|
|
||||||
/* Форм-кнопка — кнопка отправки форм записи */ |
|
||||||
#block-entityform-block-feedback button, |
|
||||||
#block-entityform-block-lor-form button { |
|
||||||
background: #FFA39C; |
|
||||||
color: white; |
|
||||||
font-weight: bold; |
|
||||||
border: solid 1px #FF847B; |
|
||||||
width: 300px; |
|
||||||
height: 42px; |
|
||||||
font-size: 18px; |
|
||||||
box-shadow: 0px 0px 5px rgba(0,0,0,0.5), 0px 4px 5px rgba(0,0,0,0.3); |
|
||||||
} |
|
||||||
|
|
||||||
/* Кнопка «Записаться на прием» в хедере */ |
|
||||||
#block-block-15 .appointment { |
|
||||||
background: #FFF; |
|
||||||
border: #BF9975 solid 1px; |
|
||||||
color: #BF9975; |
|
||||||
font-size: 14px; |
|
||||||
line-height: 38px; |
|
||||||
padding: 3px 12px; |
|
||||||
border-radius: 7px; |
|
||||||
} |
|
||||||
|
|
||||||
/* Кнопка «Позвонить» */ |
|
||||||
.show-phone { |
|
||||||
background: rgb(96, 149, 156); /* #60959c */ |
|
||||||
color: #fff; |
|
||||||
border-radius: 7px; |
|
||||||
font-size: 14px; |
|
||||||
line-height: 38px; |
|
||||||
padding: 3px 12px; |
|
||||||
} |
|
||||||
|
|
||||||
/* Кнопка «Заказать звонок» */ |
|
||||||
a.callback_url { |
|
||||||
background: #e9e4d4; |
|
||||||
border: #d5cfbd solid 1px; |
|
||||||
color: #000; |
|
||||||
border-radius: 25px; |
|
||||||
font-size: 16px; |
|
||||||
padding: 6px 18px; |
|
||||||
}`;
|
|
||||||
|
|
||||||
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> |
|
||||||
<BlockMetaBar path="/components/buttons" defaultVersion="v2.0" defaultIsInPreview={false} /> |
|
||||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Кнопки скопированы с реального сайта{" "} |
|
||||||
<span className="font-mono text-sm" style={{ color: "var(--bb-text)" }}> |
|
||||||
oclinica.ru |
|
||||||
</span> |
|
||||||
. Цвета, размеры и тени взяты напрямую из CSS темы{" "} |
|
||||||
<span className="font-mono text-sm" style={{ color: "var(--bb-text)" }}> |
|
||||||
clinic_bootstrap_mobile/css/style.css |
|
||||||
</span> |
|
||||||
. |
|
||||||
</p> |
|
||||||
<div |
|
||||||
className="mt-4 px-4 py-3 rounded-lg border text-sm flex items-center gap-2" |
|
||||||
style={{ borderColor: "#e0f5f4", background: "#f8fffe", color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
<span style={{ color: "var(--brand-053m)", fontWeight: 600 }}>Источник</span> |
|
||||||
<span> |
|
||||||
CSS сайта проанализирован 2026-03-22 — 4 типа кнопок с реальными значениями. |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* 1. Варианты */} |
|
||||||
<Section |
|
||||||
id="variants" |
|
||||||
title="Варианты" |
|
||||||
subtitle="Четыре типа кнопок с реального сайта клиники." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> |
|
||||||
{VARIANTS.map(({ variant, name, label, where, example, note, bg, border, textColor, radius, shadow }) => ( |
|
||||||
<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} size="md"> |
|
||||||
{label} |
|
||||||
</Button> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Инфо */} |
|
||||||
<div> |
|
||||||
<p className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
|
||||||
{name} |
|
||||||
</p> |
|
||||||
<p className="text-xs mb-2 leading-relaxed" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{note} |
|
||||||
</p> |
|
||||||
<div className="flex flex-wrap gap-1.5 mb-2"> |
|
||||||
{[ |
|
||||||
{ k: "bg", v: bg }, |
|
||||||
{ k: "text", v: textColor }, |
|
||||||
{ k: "border", v: border }, |
|
||||||
{ k: "radius", v: radius }, |
|
||||||
...(shadow === "да" ? [{ k: "shadow", v: "да" }] : []), |
|
||||||
].map(({ k, v }) => ( |
|
||||||
<span |
|
||||||
key={k} |
|
||||||
className="text-[10px] font-mono px-1.5 py-0.5 rounded" |
|
||||||
style={{ background: "#f3f4f6", color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
{k}: {v} |
|
||||||
</span> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
<div |
|
||||||
className="rounded p-2.5 text-xs" |
|
||||||
style={{ background: "#f8f9fa", color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
<span className="font-medium" style={{ color: "var(--bb-text)" }}> |
|
||||||
Где: |
|
||||||
</span>{" "} |
|
||||||
{where} |
|
||||||
<br /> |
|
||||||
<span className="font-medium" style={{ color: "var(--bb-text)" }}> |
|
||||||
Пример: |
|
||||||
</span>{" "} |
|
||||||
{example} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 2. Размеры */} |
|
||||||
<Section |
|
||||||
id="sizes" |
|
||||||
title="Размеры" |
|
||||||
subtitle="Три размера для разных контекстов. lg соответствует форм-кнопке на реальном сайте (18px, bold)." |
|
||||||
> |
|
||||||
<div |
|
||||||
className="rounded-xl border overflow-hidden" |
|
||||||
style={{ borderColor: "var(--bb-border)" }} |
|
||||||
> |
|
||||||
{( |
|
||||||
[ |
|
||||||
{ |
|
||||||
size: "sm" as const, |
|
||||||
label: "Small", |
|
||||||
hint: "4px 11px · 13px", |
|
||||||
use: "Компактные контексты, таблицы", |
|
||||||
}, |
|
||||||
{ |
|
||||||
size: "md" as const, |
|
||||||
label: "Medium", |
|
||||||
hint: "8px 16px · 14px", |
|
||||||
use: "Appointment, Teal, Pill (соответствует сайту)", |
|
||||||
}, |
|
||||||
{ |
|
||||||
size: "lg" as const, |
|
||||||
label: "Large", |
|
||||||
hint: "10px 24px · 18px bold", |
|
||||||
use: "Primary форм-кнопка (соответствует сайту)", |
|
||||||
}, |
|
||||||
] 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-40 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)" }}> |
|
||||||
padding: {hint.split("·")[0].trim()} · font-size: {hint.split("·")[1].trim()} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
<p |
|
||||||
className="text-xs hidden lg:block" |
|
||||||
style={{ color: "var(--bb-text-muted)", maxWidth: 220 }} |
|
||||||
> |
|
||||||
{use} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 3. Состояния */} |
|
||||||
<Section |
|
||||||
id="states" |
|
||||||
title="Состояния" |
|
||||||
subtitle="Базовые состояния кнопки. На реальном сайте hover/transition не определены в CSS." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> |
|
||||||
{( |
|
||||||
[ |
|
||||||
{ |
|
||||||
label: "Default", |
|
||||||
node: <Button variant="primary" size="lg">Записаться</Button>, |
|
||||||
hint: "Стандартное состояние", |
|
||||||
}, |
|
||||||
{ |
|
||||||
label: "Hover", |
|
||||||
node: ( |
|
||||||
<Button |
|
||||||
variant="primary" |
|
||||||
size="lg" |
|
||||||
style={{ filter: "brightness(0.93)" }} |
|
||||||
> |
|
||||||
Записаться |
|
||||||
</Button> |
|
||||||
), |
|
||||||
hint: "filter: brightness(0.93)", |
|
||||||
}, |
|
||||||
{ |
|
||||||
label: "Loading", |
|
||||||
node: <Button variant="primary" size="lg" loading>Отправка...</Button>, |
|
||||||
hint: "Спиннер + blocked", |
|
||||||
}, |
|
||||||
{ |
|
||||||
label: "Disabled", |
|
||||||
node: <Button variant="primary" size="lg" disabled>Записаться</Button>, |
|
||||||
hint: "opacity: 0.5", |
|
||||||
}, |
|
||||||
] as const |
|
||||||
).map(({ label, node, hint }) => ( |
|
||||||
<div |
|
||||||
key={label} |
|
||||||
className="rounded-xl border p-4" |
|
||||||
style={{ borderColor: "var(--bb-border)" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="flex items-center justify-center py-4 mb-3 rounded-lg" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)" }} |
|
||||||
> |
|
||||||
{node} |
|
||||||
</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> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 4. Контекст применения */} |
|
||||||
<Section |
|
||||||
id="context" |
|
||||||
title="Где применяется" |
|
||||||
subtitle="Таблица: тип кнопки → реальное использование на сайте." |
|
||||||
> |
|
||||||
<div |
|
||||||
className="rounded-xl border overflow-hidden" |
|
||||||
style={{ borderColor: "var(--bb-border)" }} |
|
||||||
> |
|
||||||
<table className="w-full text-sm"> |
|
||||||
<thead> |
|
||||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}> |
|
||||||
{["Вариант", "Цвет фона", "Реальный класс/контекст", "Текст кнопки на сайте"].map((h) => ( |
|
||||||
<th |
|
||||||
key={h} |
|
||||||
className="text-left px-4 py-3 font-medium text-xs" |
|
||||||
style={{ color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
{h} |
|
||||||
</th> |
|
||||||
))} |
|
||||||
</tr> |
|
||||||
</thead> |
|
||||||
<tbody> |
|
||||||
{[ |
|
||||||
{ |
|
||||||
v: <Button variant="primary" size="sm">Primary</Button>, |
|
||||||
bg: "#FFA39C", |
|
||||||
ctx: "button в entityform-блоках форм записи", |
|
||||||
text: "«Запишите меня!»", |
|
||||||
}, |
|
||||||
{ |
|
||||||
v: <Button variant="outline" size="sm">Outline</Button>, |
|
||||||
bg: "#fff / рамка #BF9975", |
|
||||||
ctx: ".appointment в хедере (block-block-15, 30, 32, 24)", |
|
||||||
text: "«Записаться на прием»", |
|
||||||
}, |
|
||||||
{ |
|
||||||
v: <Button variant="teal" size="sm">Teal</Button>, |
|
||||||
bg: "#60959c", |
|
||||||
ctx: ".show-phone (block-block-4, 15)", |
|
||||||
text: "«Позвонить»", |
|
||||||
}, |
|
||||||
{ |
|
||||||
v: <Button variant="pill" size="sm">Pill</Button>, |
|
||||||
bg: "#e9e4d4", |
|
||||||
ctx: "a.callback_url (modal trigger)", |
|
||||||
text: "«Заказать звонок»", |
|
||||||
}, |
|
||||||
].map(({ v, bg, ctx, text }, i) => ( |
|
||||||
<tr |
|
||||||
key={i} |
|
||||||
style={{ borderTop: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<td className="px-4 py-3">{v}</td> |
|
||||||
<td |
|
||||||
className="px-4 py-3 font-mono text-xs" |
|
||||||
style={{ color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
{bg} |
|
||||||
</td> |
|
||||||
<td |
|
||||||
className="px-4 py-3 font-mono text-xs" |
|
||||||
style={{ color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
{ctx} |
|
||||||
</td> |
|
||||||
<td className="px-4 py-3 text-xs" style={{ color: "var(--bb-text)" }}> |
|
||||||
{text} |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
))} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 5. Примеры кода */} |
|
||||||
<Section |
|
||||||
id="code" |
|
||||||
title="Примеры кода" |
|
||||||
subtitle="HTML-классы из globals.css, JSX-компонент, и точный CSS с сайта." |
|
||||||
> |
|
||||||
<div className="space-y-4"> |
|
||||||
<CodeCopy lang="HTML (CSS-классы brandbook)" code={codeHtml} /> |
|
||||||
<CodeCopy lang="JSX (React / Next.js)" code={codeReact} /> |
|
||||||
<CodeCopy lang="CSS — точно с сайта oclinica.ru" code={codeSiteExact} /> |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* LLM-блок */} |
|
||||||
<LlmBlock path="/components/buttons" version="v2.0" specText={LLM_BUTTONS_TEXT}> |
|
||||||
<LlmSection title="Варианты (реальный сайт oclinica.ru)" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Вариант", "CSS класс", "Фон", "Текст", "Border", "Radius", "Применение"]} |
|
||||||
rows={VARIANTS.map((v) => [ |
|
||||||
v.variant, |
|
||||||
v.cssClass, |
|
||||||
v.bg, |
|
||||||
v.textColor, |
|
||||||
v.border, |
|
||||||
v.radius, |
|
||||||
v.where, |
|
||||||
])} |
|
||||||
/> |
|
||||||
<LlmSection title="Размеры (брендбук-компонент)" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Размер", "padding", "font-size", "Применение"]} |
|
||||||
rows={[ |
|
||||||
["sm", "4px 11px", "13px", "Компактные контексты"], |
|
||||||
["md", "8px 16px", "14px", "Стандарт (outline, teal, pill с сайта)"], |
|
||||||
["lg", "10px 24px", "18px bold", "Primary форм-кнопка (соответствует сайту)"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила применения" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "primary (коралловый) — только для submit в формах записи" }, |
|
||||||
{ ok: true, text: "outline (бежевый) — хедер, навигация, второстепенные ссылки" }, |
|
||||||
{ ok: true, text: "teal (бирюзовый) — телефонные и контактные действия" }, |
|
||||||
{ ok: true, text: "pill (кремовый) — открытие модальных окон / callback" }, |
|
||||||
{ ok: true, text: "Не более одного primary на форму" }, |
|
||||||
{ ok: false, text: "Не менять цвета вне указанной палитры сайта" }, |
|
||||||
{ ok: false, text: "Primary — не для навигационных ссылок" }, |
|
||||||
{ ok: false, text: "Не накладывать тень на outline, teal, pill" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,631 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { CodeCopy } from "@/components/ui/CodeCopy"; |
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
|
|
||||||
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-14"> |
|
||||||
<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 DoctorCard({ |
|
||||||
name, specialty, experience, photo, |
|
||||||
}: { |
|
||||||
name: string; specialty: string; experience: string; photo?: string; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="flex gap-4 p-4 rounded-xl border bg-white transition-shadow" |
|
||||||
style={{ borderColor: "var(--bb-border)" }} |
|
||||||
> |
|
||||||
{/* Фото */} |
|
||||||
<div |
|
||||||
className="shrink-0 rounded-lg overflow-hidden" |
|
||||||
style={{ width: 110, height: 160, background: "#dff0fa" }} |
|
||||||
> |
|
||||||
{photo ? ( |
|
||||||
<img src={photo} alt={name} style={{ width: "100%", height: "100%", objectFit: "cover" }} /> |
|
||||||
) : ( |
|
||||||
<div |
|
||||||
className="w-full h-full flex flex-col items-center justify-center gap-1" |
|
||||||
style={{ color: "var(--brand-053m)", opacity: 0.5 }} |
|
||||||
> |
|
||||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"> |
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> |
|
||||||
<circle cx="12" cy="7" r="4" /> |
|
||||||
</svg> |
|
||||||
<span className="text-xs">фото</span> |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Информация */} |
|
||||||
<div className="flex flex-col justify-between min-w-0"> |
|
||||||
<div> |
|
||||||
<h3 className="font-semibold text-sm leading-tight mb-1" style={{ color: "var(--bb-text)" }}> |
|
||||||
{name} |
|
||||||
</h3> |
|
||||||
<p className="text-xs mb-2" style={{ color: "var(--bb-text-muted)" }}>{specialty}</p> |
|
||||||
<p className="text-xs" style={{ color: "var(--brand-053m)" }}>{experience}</p> |
|
||||||
</div> |
|
||||||
<button className="bb-btn bb-btn-sm bb-btn-outline mt-3 self-start"> |
|
||||||
Записаться |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/* ─── Карточка новости ──────────────────────────────────────────────── */ |
|
||||||
function NewsCard({ |
|
||||||
date, title, snippet, category, |
|
||||||
}: { |
|
||||||
date: string; title: string; snippet: string; category?: string; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="bb-news-card rounded-xl border overflow-hidden cursor-pointer transition-all" |
|
||||||
style={{ borderColor: "var(--bb-border)", background: "#fff" }} |
|
||||||
> |
|
||||||
{/* Превью */} |
|
||||||
<div |
|
||||||
className="h-36 flex items-center justify-center" |
|
||||||
style={{ background: "#f0f9ff" }} |
|
||||||
> |
|
||||||
{category && ( |
|
||||||
<span |
|
||||||
className="px-3 py-1 rounded-full text-xs font-semibold" |
|
||||||
style={{ background: "var(--brand-053m)", color: "#fff" }} |
|
||||||
> |
|
||||||
{category} |
|
||||||
</span> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Контент */} |
|
||||||
<div className="p-4"> |
|
||||||
<p className="text-xs mb-2" style={{ color: "var(--bb-text-muted)" }}>{date}</p> |
|
||||||
<h3 className="font-semibold text-sm leading-tight mb-2" style={{ color: "var(--bb-text)" }}> |
|
||||||
{title} |
|
||||||
</h3> |
|
||||||
<p className="text-xs leading-relaxed mb-3" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{snippet} |
|
||||||
</p> |
|
||||||
<span className="text-xs font-medium" style={{ color: "var(--brand-053m)" }}> |
|
||||||
Читать далее → |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/* ─── Карточка отзыва ───────────────────────────────────────────────── */ |
|
||||||
function ReviewCard({ |
|
||||||
author, date, text, rating, doctor, |
|
||||||
}: { |
|
||||||
author: string; date: string; text: string; rating: number; doctor?: string; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="rounded-xl border p-5" |
|
||||||
style={{ borderColor: "var(--bb-border)", background: "#eef4d1" }} |
|
||||||
> |
|
||||||
{/* Звёзды */} |
|
||||||
<div className="flex gap-0.5 mb-3"> |
|
||||||
{Array.from({ length: 5 }, (_, i) => ( |
|
||||||
<svg key={i} width="16" height="16" viewBox="0 0 24 24" fill={i < rating ? "#f59e0b" : "none"} stroke="#f59e0b" strokeWidth="1.5"> |
|
||||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" /> |
|
||||||
</svg> |
|
||||||
))} |
|
||||||
<span className="text-xs ml-1 font-medium" style={{ color: "#92400e" }}>{rating}/5</span> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Текст */} |
|
||||||
<p |
|
||||||
className="text-sm leading-relaxed mb-4" |
|
||||||
style={{ |
|
||||||
color: "var(--bb-text)", |
|
||||||
display: "-webkit-box", |
|
||||||
WebkitLineClamp: 4, |
|
||||||
WebkitBoxOrient: "vertical", |
|
||||||
overflow: "hidden", |
|
||||||
}} |
|
||||||
> |
|
||||||
{text} |
|
||||||
</p> |
|
||||||
|
|
||||||
{/* Автор */} |
|
||||||
<div className="flex items-end justify-between"> |
|
||||||
<div> |
|
||||||
<p className="text-sm font-semibold" style={{ color: "var(--bb-text)" }}>{author}</p> |
|
||||||
{doctor && <p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>Врач: {doctor}</p>} |
|
||||||
</div> |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{date}</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/* ─── Карточка цены ─────────────────────────────────────────────────── */ |
|
||||||
function PriceCard({ |
|
||||||
service, price, description, highlighted, |
|
||||||
}: { |
|
||||||
service: string; price: string; description?: string; highlighted?: boolean; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="rounded-xl border p-5 flex flex-col gap-3" |
|
||||||
style={{ |
|
||||||
borderColor: highlighted ? "var(--brand-053m)" : "var(--bb-border)", |
|
||||||
background: highlighted ? "#f0f9ff" : "#fff", |
|
||||||
}} |
|
||||||
> |
|
||||||
<div className="flex items-start justify-between gap-3"> |
|
||||||
<p className="text-sm font-medium flex-1" style={{ color: "var(--bb-text)" }}>{service}</p> |
|
||||||
<p className="text-lg font-bold shrink-0" style={{ color: highlighted ? "var(--brand-053m)" : "var(--bb-text)" }}> |
|
||||||
{price} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
{description && ( |
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{description}</p> |
|
||||||
)} |
|
||||||
<button className="bb-btn bb-btn-sm bb-btn-outline self-start"> |
|
||||||
Записаться |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/* ─── Карточка услуги ───────────────────────────────────────────────── */ |
|
||||||
function ServiceCard({ |
|
||||||
title, description, icon, |
|
||||||
}: { |
|
||||||
title: string; description: string; icon: string; |
|
||||||
}) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="bb-service-card rounded-xl border p-5 flex flex-col gap-3 cursor-pointer transition-shadow" |
|
||||||
style={{ borderColor: "var(--bb-border)", background: "#fff" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" |
|
||||||
style={{ background: "#dff0fa" }} |
|
||||||
> |
|
||||||
{icon} |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
<h3 className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}>{title}</h3> |
|
||||||
<p className="text-xs leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>{description}</p> |
|
||||||
</div> |
|
||||||
<span className="text-xs font-medium" style={{ color: "var(--brand-053m)" }}> |
|
||||||
Подробнее → |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/* ─── LLM текст ─────────────────────────────────────────────────────── */ |
|
||||||
const LLM_CARDS_TEXT = `КАРТОЧКИ — LLM-спецификация
|
|
||||||
Версия: v1.0 · /components/cards |
|
||||||
|
|
||||||
КАРТОЧКА ВРАЧА (DoctorCard) |
|
||||||
Структура: фото (110×160px) + имя + специализация + опыт + кнопка «Записаться» |
|
||||||
Фото: 110px × 160px, object-fit: cover, border-radius: 8px, фон-placeholder: #dff0fa |
|
||||||
Кнопка: .bb-btn-outline .bb-btn-sm |
|
||||||
Источник: .doctor на perm.oclinica.ru |
|
||||||
|
|
||||||
КАРТОЧКА НОВОСТИ (NewsCard) |
|
||||||
Структура: превью (h=144px) + дата + заголовок + анонс (4 строки) + «Читать далее →» |
|
||||||
Hover: background #eef4d1 + box-shadow 0 0 16px #9e9e9a |
|
||||||
Источник: #block-views-last-news-block-1 .views-column на сайте (200×200px) |
|
||||||
Без hover: background #fff, border 1px |
|
||||||
|
|
||||||
КАРТОЧКА ОТЗЫВА (ReviewCard) |
|
||||||
Структура: рейтинг (звёзды 1–5) + текст (4 строки, overflow hidden) + автор + дата + врач |
|
||||||
Фон: #eef4d1 (светло-жёлтый) — из реального CSS сайта |
|
||||||
Звёзды: SVG polygon, filled #f59e0b |
|
||||||
|
|
||||||
КАРТОЧКА ЦЕНЫ (PriceCard) |
|
||||||
Структура: услуга + цена (bold) + описание + кнопка |
|
||||||
highlighted-вариант: border var(--brand-053m), bg #f0f9ff, цена тоже --brand-053m |
|
||||||
|
|
||||||
КАРТОЧКА УСЛУГИ (ServiceCard) |
|
||||||
Структура: иконка (emoji, 48×48px, bg #dff0fa) + заголовок + описание + «Подробнее →» |
|
||||||
Hover: box-shadow (0.5rem smth) |
|
||||||
|
|
||||||
БЕЙДЖИ (Badge) |
|
||||||
Варианты: primary (#0089c3), success (#059669), warning (#d97706), danger (#dc2626), neutral (#6b7280) |
|
||||||
Размер: text-xs, px-2.5 py-0.5, border-radius: full |
|
||||||
CSS: inline-flex, font-weight: 600 |
|
||||||
|
|
||||||
ТЕГИ (Tag) |
|
||||||
Варианты: default (border, text), filled (bg), removable (с кнопкой ×) |
|
||||||
Цвет: --brand-053m или нейтральный (#e5e7eb bg) |
|
||||||
Размер: text-xs, px-2 py-1, border-radius: 4px |
|
||||||
|
|
||||||
АЛЕРТЫ (Alert) |
|
||||||
Варианты: info (#dff0fa фон, #075985 текст), success (#d1fae5, #065f46), warning (#fef3c7, #92400e), error (#fee2e2, #991b1b) |
|
||||||
Структура: иконка (16×16px) + заголовок + описание |
|
||||||
Без кнопки закрыть в базовом варианте |
|
||||||
|
|
||||||
ПРАВИЛА |
|
||||||
✓ DoctorCard: всегда показывать фото-placeholder если нет фото (не ломать layout) |
|
||||||
✓ ReviewCard: обрезать текст после 4 строк (WebkitLineClamp) |
|
||||||
✓ PriceCard: highlighted = акционная или рекомендуемая позиция |
|
||||||
✓ NewsCard: hover-эффект обязателен (#eef4d1 + box-shadow) |
|
||||||
✓ Badge: не более 2–3 бейджей рядом |
|
||||||
✓ Alert: одновременно не более 1 alert одного типа на экране |
|
||||||
✕ Не смешивать типы карточек в одной сетке без заголовка секции |
|
||||||
✕ Не использовать ReviewCard без рейтинга`.trim();
|
|
||||||
|
|
||||||
/* ─── Данные примеров ───────────────────────────────────────────────── */ |
|
||||||
const DOCTORS = [ |
|
||||||
{ |
|
||||||
name: "Иванова Анна Сергеевна", |
|
||||||
specialty: "Оториноларинголог (ЛОР), высшая категория", |
|
||||||
experience: "Стаж 18 лет", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Петров Дмитрий Александрович", |
|
||||||
specialty: "Сурдолог, аудиолог", |
|
||||||
experience: "Стаж 12 лет", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Соколова Мария Ивановна", |
|
||||||
specialty: "Детский ЛОР, ринолог", |
|
||||||
experience: "Стаж 9 лет", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const REVIEWS = [ |
|
||||||
{ |
|
||||||
author: "Елена К.", |
|
||||||
date: "15 марта 2026", |
|
||||||
rating: 5, |
|
||||||
doctor: "Иванова А.С.", |
|
||||||
text: "Очень довольна визитом. Доктор внимательно выслушала все жалобы, провела осмотр и объяснила причину заболевания. Назначила лечение, которое помогло уже через 3 дня. Рекомендую!", |
|
||||||
}, |
|
||||||
{ |
|
||||||
author: "Михаил Р.", |
|
||||||
date: "10 марта 2026", |
|
||||||
rating: 4, |
|
||||||
doctor: "Петров Д.А.", |
|
||||||
text: "Хороший специалист, всё объяснил понятно. Подождал немного дольше, чем ожидал, но качество приёма на высоте. Буду обращаться снова.", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const PRICES = [ |
|
||||||
{ service: "Первичный приём ЛОР-врача", price: "1 500 ₽", description: "Включает осмотр и консультацию" }, |
|
||||||
{ service: "Повторный приём", price: "1 000 ₽", description: "До 14 дней после первичного" }, |
|
||||||
{ service: "Промывание миндалин", price: "800 ₽", highlighted: true, description: "Аппаратное — аккустический вакуум" }, |
|
||||||
{ service: "Аудиометрия", price: "1 200 ₽", description: "Исследование слуха" }, |
|
||||||
]; |
|
||||||
|
|
||||||
const SERVICES = [ |
|
||||||
{ title: "Лечение ангины и тонзиллита", description: "Консервативное и хирургическое лечение заболеваний миндалин", icon: "🦷" }, |
|
||||||
{ title: "Аудиология и сурдология", description: "Диагностика нарушений слуха, подбор слуховых аппаратов", icon: "👂" }, |
|
||||||
{ title: "Детский ЛОР", description: "Специализация на лечении ЛОР-заболеваний у детей от 0 лет", icon: "👶" }, |
|
||||||
{ title: "Ринология", description: "Лечение заболеваний носа и придаточных пазух", icon: "👃" }, |
|
||||||
]; |
|
||||||
|
|
||||||
/* ─── Коды примеров ─────────────────────────────────────────────────── */ |
|
||||||
const codeDoctorCard = `<!-- Карточка врача -->
|
|
||||||
<div class="doctor-card"> |
|
||||||
<img src="doctor.jpg" width="110" height="160" alt="ФИО" /> |
|
||||||
<div class="doctor-info"> |
|
||||||
<h3>Иванова Анна Сергеевна</h3> |
|
||||||
<p class="specialty">Оториноларинголог, высшая категория</p> |
|
||||||
<p class="experience">Стаж 18 лет</p> |
|
||||||
<button class="bb-btn bb-btn-sm bb-btn-outline">Записаться</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
/* CSS с сайта oclinica.ru */ |
|
||||||
.doctor .image { float:left; margin-right:20px; width:110px; height:160px; } |
|
||||||
.doctor .item { float:left; width:170px; } |
|
||||||
.doctor h3 { margin-top:0; height:32px; }`;
|
|
||||||
|
|
||||||
const codeNewsCard = `<!-- Карточка новости (сайт: 200×200px) -->
|
|
||||||
<div class="news-card"> |
|
||||||
<div class="news-preview">...</div> |
|
||||||
<div class="news-body"> |
|
||||||
<time>15 марта 2026</time> |
|
||||||
<h3>Заголовок новости</h3> |
|
||||||
<p>Краткий анонс...</p> |
|
||||||
<a href="#">Читать далее →</a> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
/* CSS с сайта */ |
|
||||||
#block-views-last-news-block-1 .views-column { |
|
||||||
background: #fff; width: 200px; height: 200px; |
|
||||||
margin: 15px 8px; padding: 15px; |
|
||||||
} |
|
||||||
#block-views-last-news-block-1 .views-column:hover { |
|
||||||
background: #eef4d1; |
|
||||||
box-shadow: 0px 0px 16px 0px #9e9e9a; |
|
||||||
}`;
|
|
||||||
|
|
||||||
const codeReviewCard = `<!-- Карточка отзыва -->
|
|
||||||
<div class="review-card"> |
|
||||||
<div class="stars">★★★★★</div> |
|
||||||
<p class="text">Текст отзыва (4 строки, overflow: hidden)...</p> |
|
||||||
<div class="author"> |
|
||||||
<span>Елена К.</span> |
|
||||||
<time>15 марта 2026</time> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
/* Стиль брендбука (фон из CSS сайта) */ |
|
||||||
.review-card { background: #eef4d1; border-radius: 12px; padding: 20px; }`;
|
|
||||||
|
|
||||||
const codePriceCard = `<!-- Карточка цены -->
|
|
||||||
<div class="price-card"> |
|
||||||
<div class="price-row"> |
|
||||||
<span class="service">Первичный приём ЛОР-врача</span> |
|
||||||
<strong class="price">1 500 ₽</strong> |
|
||||||
</div> |
|
||||||
<p class="description">Включает осмотр и консультацию</p> |
|
||||||
<button class="bb-btn bb-btn-sm bb-btn-outline">Записаться</button> |
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const codeBadges = `<!-- Бейджи -->
|
|
||||||
<span class="bb-badge bb-badge-primary">ЛОР</span> |
|
||||||
<span class="bb-badge bb-badge-success">Принимает</span> |
|
||||||
<span class="bb-badge bb-badge-warning">Ожидает</span> |
|
||||||
<span class="bb-badge bb-badge-danger">Не принимает</span> |
|
||||||
<span class="bb-badge bb-badge-neutral">Высшая категория</span> |
|
||||||
|
|
||||||
<!-- Теги --> |
|
||||||
<button class="bb-tag">Ухо</button> |
|
||||||
<button class="bb-tag">Горло</button> |
|
||||||
<button class="bb-tag bb-tag-active">Нос</button> |
|
||||||
|
|
||||||
<!-- Алерт --> |
|
||||||
<div class="bb-alert bb-alert-info"> |
|
||||||
<span class="bb-alert-icon">ℹ</span> |
|
||||||
<div> |
|
||||||
<strong>Запись открыта</strong> |
|
||||||
<p>Вы можете записаться онлайн или по телефону.</p> |
|
||||||
</div> |
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default function CardsPage() { |
|
||||||
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.3 |
|
||||||
</p> |
|
||||||
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
|
||||||
Карточки |
|
||||||
</h1> |
|
||||||
<BlockMetaBar path="/components/cards" defaultVersion="v1.0" defaultIsInPreview={false} /> |
|
||||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Карточки врача, новости, отзыва, цены и услуги — основные блоки контента сайта. |
|
||||||
Бейджи, теги и алерты — вспомогательные элементы. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* 1. Карточки врачей */} |
|
||||||
<Section |
|
||||||
id="doctor" |
|
||||||
title="Карточка врача" |
|
||||||
subtitle="Фото 110×160px, имя, специализация, стаж, кнопка записи. Соответствует .doctor на сайте." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> |
|
||||||
{DOCTORS.map(d => <DoctorCard key={d.name} {...d} />)} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 2. Карточки новостей */} |
|
||||||
<Section |
|
||||||
id="news" |
|
||||||
title="Карточка новости" |
|
||||||
subtitle="Hover: bg #eef4d1 + box-shadow. Источник: #block-views-last-news-block-1 на сайте." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> |
|
||||||
<NewsCard |
|
||||||
date="15 марта 2026" |
|
||||||
category="ЛОР" |
|
||||||
title="Как правильно промывать нос при насморке" |
|
||||||
snippet="Промывание носа — эффективный метод лечения и профилактики острого ринита. Рассказываем о правильной технике." |
|
||||||
/> |
|
||||||
<NewsCard |
|
||||||
date="10 марта 2026" |
|
||||||
category="Аудиология" |
|
||||||
title="Новый слуховой аппарат в нашей клинике" |
|
||||||
snippet="Мы начали подбор и настройку слуховых аппаратов нового поколения — незаметных и точных." |
|
||||||
/> |
|
||||||
<NewsCard |
|
||||||
date="5 марта 2026" |
|
||||||
title="Весенняя профилактика ЛОР-заболеваний" |
|
||||||
snippet="Апрель — период обострений. Рекомендации нашего специалиста по укреплению иммунитета и защите." |
|
||||||
/> |
|
||||||
</div> |
|
||||||
<p className="mt-3 text-xs" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
* Наведите на карточку чтобы увидеть hover-эффект |
|
||||||
</p> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 3. Карточки отзывов */} |
|
||||||
<Section |
|
||||||
id="review" |
|
||||||
title="Карточка отзыва" |
|
||||||
subtitle="Рейтинг (1–5 звёзд), текст 4 строки, автор, дата, врач. Фон #eef4d1 — с реального сайта." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
|
||||||
{REVIEWS.map(r => <ReviewCard key={r.author} {...r} />)} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 4. Карточки цен */} |
|
||||||
<Section |
|
||||||
id="price" |
|
||||||
title="Карточка цены" |
|
||||||
subtitle="Услуга + стоимость + описание + кнопка. Highlighted-вариант для акционных позиций." |
|
||||||
> |
|
||||||
<div className="flex flex-col gap-3"> |
|
||||||
{PRICES.map(p => <PriceCard key={p.service} {...p} />)} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 5. Карточки услуг */} |
|
||||||
<Section |
|
||||||
id="service" |
|
||||||
title="Карточка услуги" |
|
||||||
subtitle="Иконка + заголовок + описание + ссылка. Применяется в блоке «Наши услуги»." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4"> |
|
||||||
{SERVICES.map(s => <ServiceCard key={s.title} {...s} />)} |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 6. Бейджи и теги */} |
|
||||||
<Section |
|
||||||
id="badges" |
|
||||||
title="Бейджи и теги" |
|
||||||
subtitle="Статусные бейджи, теги-категории, алерты." |
|
||||||
> |
|
||||||
{/* Бейджи */} |
|
||||||
<div className="mb-8"> |
|
||||||
<p className="text-sm font-medium mb-3" style={{ color: "var(--bb-text)" }}>Бейджи (статус)</p> |
|
||||||
<div className="flex flex-wrap gap-2"> |
|
||||||
{[ |
|
||||||
{ label: "Основной", bg: "var(--brand-053m)", color: "#fff" }, |
|
||||||
{ label: "Принимает", bg: "#059669", color: "#fff" }, |
|
||||||
{ label: "Высшая категория", bg: "#d97706", color: "#fff" }, |
|
||||||
{ label: "Не принимает", bg: "#dc2626", color: "#fff" }, |
|
||||||
{ label: "Нейтральный", bg: "#6b7280", color: "#fff" }, |
|
||||||
{ label: "Новинка", bg: "#dff0fa", color: "var(--brand-053m)" }, |
|
||||||
].map(b => ( |
|
||||||
<span |
|
||||||
key={b.label} |
|
||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold" |
|
||||||
style={{ background: b.bg, color: b.color }} |
|
||||||
> |
|
||||||
{b.label} |
|
||||||
</span> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Теги */} |
|
||||||
<div className="mb-8"> |
|
||||||
<p className="text-sm font-medium mb-3" style={{ color: "var(--bb-text)" }}>Теги (категории)</p> |
|
||||||
<div className="flex flex-wrap gap-2"> |
|
||||||
{["Ухо", "Горло", "Нос", "Аудиология", "Детский ЛОР", "Хирургия"].map((tag, i) => ( |
|
||||||
<button |
|
||||||
key={tag} |
|
||||||
className="inline-flex items-center px-3 py-1 rounded text-xs font-medium border transition-colors" |
|
||||||
style={ |
|
||||||
i === 0 |
|
||||||
? { background: "var(--brand-053m)", color: "#fff", borderColor: "var(--brand-053m)" } |
|
||||||
: { background: "#fff", color: "var(--bb-text)", borderColor: "var(--bb-border)" } |
|
||||||
} |
|
||||||
> |
|
||||||
{tag} |
|
||||||
</button> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Алерты */} |
|
||||||
<div> |
|
||||||
<p className="text-sm font-medium mb-3" style={{ color: "var(--bb-text)" }}>Алерты</p> |
|
||||||
<div className="flex flex-col gap-3"> |
|
||||||
{[ |
|
||||||
{ type: "info", bg: "#dff0fa", color: "#075985", icon: "ℹ", title: "Информация", text: "Запись открыта. Вы можете записаться онлайн или по телефону." }, |
|
||||||
{ type: "success", bg: "#d1fae5", color: "#065f46", icon: "✓", title: "Успешно", text: "Ваша запись подтверждена. Ждём вас 20 марта в 10:00." }, |
|
||||||
{ type: "warning", bg: "#fef3c7", color: "#92400e", icon: "⚠", title: "Внимание", text: "Не забудьте взять паспорт и полис ОМС на приём." }, |
|
||||||
{ type: "error", bg: "#fee2e2", color: "#991b1b", icon: "✕", title: "Ошибка", text: "К сожалению, это время уже занято. Выберите другое." }, |
|
||||||
].map(a => ( |
|
||||||
<div |
|
||||||
key={a.type} |
|
||||||
className="flex gap-3 p-4 rounded-xl border" |
|
||||||
style={{ background: a.bg, borderColor: a.bg, color: a.color }} |
|
||||||
> |
|
||||||
<span className="text-base font-bold shrink-0 mt-0.5">{a.icon}</span> |
|
||||||
<div> |
|
||||||
<p className="text-sm font-semibold mb-0.5">{a.title}</p> |
|
||||||
<p className="text-sm opacity-90">{a.text}</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 7. Код */} |
|
||||||
<Section |
|
||||||
id="code" |
|
||||||
title="Примеры кода" |
|
||||||
subtitle="HTML-структура и CSS-справка." |
|
||||||
> |
|
||||||
<div className="space-y-4"> |
|
||||||
<CodeCopy lang="HTML — DoctorCard + CSS с сайта" code={codeDoctorCard} /> |
|
||||||
<CodeCopy lang="HTML — NewsCard + CSS с сайта" code={codeNewsCard} /> |
|
||||||
<CodeCopy lang="HTML — ReviewCard" code={codeReviewCard} /> |
|
||||||
<CodeCopy lang="HTML — PriceCard" code={codePriceCard} /> |
|
||||||
<CodeCopy lang="HTML — Badges, Tags, Alerts" code={codeBadges} /> |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* LLM-блок */} |
|
||||||
<LlmBlock path="/components/cards" version="v1.0" specText={LLM_CARDS_TEXT}> |
|
||||||
<LlmSection title="Типы карточек" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Карточка", "Ключевые размеры", "Источник на сайте", "Фон / hover"]} |
|
||||||
rows={[ |
|
||||||
["DoctorCard", "фото 110×160px, layout: flex", ".doctor .image + .doctor .item", "#fff / —"], |
|
||||||
["NewsCard", "preview h=144px, grid 3 col", "#block-views-last-news-block-1 .views-column", "#fff / #eef4d1 + shadow"], |
|
||||||
["ReviewCard", "4 строки текста, рейтинг", ".node-reviews", "#eef4d1 / —"], |
|
||||||
["PriceCard", "flex row: name + price", ".field-name-field-price-priem", "#fff / highlighted: #f0f9ff"], |
|
||||||
["ServiceCard", "иконка 48×48, grid 4 col", "—", "#fff / shadow"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Бейджи, теги, алерты" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "Варианты", "Размер", "Применение"]} |
|
||||||
rows={[ |
|
||||||
["Badge", "primary / success / warning / danger / neutral", "text-xs, px-2.5, rounded-full", "Статус врача, категория, акция"], |
|
||||||
["Tag", "default / active", "text-xs, px-3, rounded-4px", "Фильтры, категории услуг"], |
|
||||||
["Alert", "info / success / warning / error", "p-4, border-radius 12px", "Системные сообщения пользователю"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила применения" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "DoctorCard: всегда фото-placeholder если нет фото" }, |
|
||||||
{ ok: true, text: "NewsCard: hover #eef4d1 + box-shadow (из реального CSS сайта)" }, |
|
||||||
{ ok: true, text: "ReviewCard: обрезать текст после 4 строк (WebkitLineClamp: 4)" }, |
|
||||||
{ ok: true, text: "PriceCard highlighted = акционная / рекомендуемая позиция" }, |
|
||||||
{ ok: true, text: "Alert: один тип одновременно на экране" }, |
|
||||||
{ ok: false, text: "Не смешивать типы карточек в одной сетке без заголовка" }, |
|
||||||
{ ok: false, text: "Не использовать ReviewCard без рейтинга" }, |
|
||||||
{ ok: false, text: "Не ставить более 3 бейджей рядом" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
|
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,725 +0,0 @@ |
|||||||
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"; |
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; |
|
||||||
|
|
||||||
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-спецификация
|
|
||||||
Версия: v2.0 · /components/forms |
|
||||||
|
|
||||||
ТЕКСТОВОЕ ПОЛЕ (Input) |
|
||||||
CSS класс: .bb-input |
|
||||||
Высота: 50px · padding: 10px 12px |
|
||||||
border: 1px solid #ccc · border-radius: 4px · font: Fira Sans 14px |
|
||||||
Источник: entityform input[type=text] на perm.oclinica.ru |
|
||||||
Состояния: |
|
||||||
default: border #ccc |
|
||||||
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 · padding: 10px 12px |
|
||||||
|
|
||||||
ВЫПАДАЮЩИЙ СПИСОК (Select) |
|
||||||
CSS класс: .bb-select |
|
||||||
Высота: 50px · padding: 10px 36px 10px 10px |
|
||||||
Стрелка: SVG background-image (data URI) |
|
||||||
Источник: .form-control.form-select entityform на сайте |
|
||||||
Те же состояния что у 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" /> |
|
||||||
|
|
||||||
КОНТЕКСТ ПРИМЕНЕНИЯ НА САЙТЕ |
|
||||||
Input/Select используются в entityform-блоках: |
|
||||||
#block-entityform-block-lor-form — форма «Запишите меня!» (ЛОР) |
|
||||||
#block-entityform-block-lor-form-2 — форма «Узнайте стоимость операции» |
|
||||||
#block-entityform-block-surgery-form — форма хирургии |
|
||||||
Фон формы: #b8e6ed (светло-бирюзовый) |
|
||||||
Ширина полей: 302px (фиксированная), кнопка submit: 300px |
|
||||||
|
|
||||||
ОБЩИЕ ПРАВИЛА |
|
||||||
✓ Метка (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="Настройка недоступна" />`;
|
|
||||||
|
|
||||||
const codeSiteCSS = `/* ── Реальный CSS с perm.oclinica.ru ─────────────────────────── */
|
|
||||||
|
|
||||||
/* Базовые стили (Bootstrap override) */ |
|
||||||
input[type=text], |
|
||||||
input[type=email] { |
|
||||||
padding: 0; |
|
||||||
height: 30px; |
|
||||||
border: 1px solid #ccc; |
|
||||||
} |
|
||||||
|
|
||||||
/* Entityform-блоки: форма записи на приём / узнать стоимость */ |
|
||||||
#block-entityform-block-lor-form input[type=text], |
|
||||||
#block-entityform-block-lor-form-2 input[type=text], |
|
||||||
#block-entityform-block-surgery-form input[type=text] { |
|
||||||
height: 50px; |
|
||||||
padding: 10px; |
|
||||||
} |
|
||||||
|
|
||||||
/* Select в entityform */ |
|
||||||
.field-name-field-lor-vrach .form-control.form-select { |
|
||||||
height: 50px; |
|
||||||
padding: 10px 16px; |
|
||||||
font-size: .9em; |
|
||||||
font-weight: bold; |
|
||||||
font-family: 'Fira Sans'; |
|
||||||
color: #949290; |
|
||||||
} |
|
||||||
|
|
||||||
/* Webform (отдельный вид форм) — скруглений нет */ |
|
||||||
.webform-client-form input[type=text].form-text { |
|
||||||
border-radius: 0; |
|
||||||
}`;
|
|
||||||
|
|
||||||
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> |
|
||||||
<BlockMetaBar path="/components/forms" defaultVersion="v2.0" defaultIsInPreview={false} /> |
|
||||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Элементы ввода данных: текстовые поля, выпадающие списки, флажки, переключатели. |
|
||||||
Применяются в формах записи, фильтрах и настройках. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* 1. Input */} |
|
||||||
<Section |
|
||||||
id="input" |
|
||||||
title="Текстовое поле" |
|
||||||
subtitle="Базовый элемент ввода текста. Класс .bb-input. Высота 50px — как на сайте oclinica.ru." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4"> |
|
||||||
<StateCard label="Default" hint="border: 1px solid #ccc · border-radius: 4px · height: 50px"> |
|
||||||
<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. border: 1px solid #ccc · border-radius: 4px." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
|
||||||
<StateCard label="Default" hint="min-height: 100px · 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. Высота 50px — как в entityform на сайте." |
|
||||||
> |
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
|
||||||
<StateCard label="Default" hint="height: 50px · кастомная стрелка SVG"> |
|
||||||
<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="context" |
|
||||||
title="Контекст применения" |
|
||||||
subtitle="Как форм-контролы выглядят на сайте oclinica.ru — в реальных entityform-блоках." |
|
||||||
> |
|
||||||
<div className="overflow-x-auto mb-6"> |
|
||||||
<table className="w-full text-sm border-collapse"> |
|
||||||
<thead> |
|
||||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}> |
|
||||||
{["Контрол", "CSS класс", "Где на сайте", "CSS-блок на сайте"].map((h) => ( |
|
||||||
<th |
|
||||||
key={h} |
|
||||||
className="text-left px-3 py-2 font-semibold text-xs uppercase tracking-wide" |
|
||||||
style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
{h} |
|
||||||
</th> |
|
||||||
))} |
|
||||||
</tr> |
|
||||||
</thead> |
|
||||||
<tbody> |
|
||||||
{[ |
|
||||||
["Input (text)", ".bb-input", "Форма записи ЛОР, форма хирургии, «Узнайте стоимость»", "#block-entityform-block-lor-form input[type=text]"], |
|
||||||
["Select", ".bb-select", "Выбор врача в форме записи", ".field-name-field-lor-vrach .form-control.form-select"], |
|
||||||
["Checkbox", ".bb-checkbox", "Согласие на обработку данных в entityform", ".form-type-checkbox.checkbox label"], |
|
||||||
["Textarea", ".bb-textarea", "Комментарии (в ряде форм)", "Без специального CSS на сайте (Bootstrap)"], |
|
||||||
["Toggle", ".bb-toggle-track", "Не используется на сайте (UI-компонент брендбука)", "—"], |
|
||||||
].map(([ctrl, cls, where, block]) => ( |
|
||||||
<tr key={ctrl} style={{ borderBottom: "1px solid var(--bb-border)" }}> |
|
||||||
<td className="px-3 py-2.5 font-medium" style={{ color: "var(--bb-text)" }}>{ctrl}</td> |
|
||||||
<td className="px-3 py-2.5"> |
|
||||||
<code className="text-xs px-1.5 py-0.5 rounded" style={{ background: "var(--bb-sidebar-bg)", color: "var(--brand-073m)" }}>{cls}</code> |
|
||||||
</td> |
|
||||||
<td className="px-3 py-2.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>{where}</td> |
|
||||||
<td className="px-3 py-2.5"> |
|
||||||
<code className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{block}</code> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
))} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Макет формы как на сайте */} |
|
||||||
<div className="rounded-xl overflow-hidden border" style={{ borderColor: "var(--bb-border)" }}> |
|
||||||
<div className="px-4 py-2 text-xs font-semibold uppercase tracking-widest" style={{ background: "var(--bb-sidebar-bg)", color: "var(--bb-text-muted)" }}> |
|
||||||
Макет — entityform «Узнайте стоимость операции» (oclinica.ru/lor) |
|
||||||
</div> |
|
||||||
<div style={{ background: "#b8e6ed", padding: "32px 24px" }}> |
|
||||||
<div |
|
||||||
className="mx-auto" |
|
||||||
style={{ |
|
||||||
maxWidth: 340, |
|
||||||
background: "#b8e6ed", |
|
||||||
textAlign: "center", |
|
||||||
}} |
|
||||||
> |
|
||||||
<p className="text-sm font-semibold mb-4" style={{ color: "#333", fontFamily: "var(--font-web)" }}> |
|
||||||
Узнайте стоимость операции |
|
||||||
</p> |
|
||||||
<div className="flex flex-col gap-3" style={{ alignItems: "center" }}> |
|
||||||
<input |
|
||||||
className="bb-input" |
|
||||||
type="text" |
|
||||||
placeholder="Ваше имя" |
|
||||||
style={{ width: 302 }} |
|
||||||
readOnly |
|
||||||
/> |
|
||||||
<input |
|
||||||
className="bb-input" |
|
||||||
type="text" |
|
||||||
placeholder="Ваш телефон" |
|
||||||
style={{ width: 302 }} |
|
||||||
readOnly |
|
||||||
/> |
|
||||||
<select className="bb-select" style={{ width: 302 }}> |
|
||||||
<option>Выберите врача</option> |
|
||||||
<option>Иванов И.И.</option> |
|
||||||
</select> |
|
||||||
<button className="bb-btn bb-btn-lg bb-btn-primary" style={{ width: 300 }}> |
|
||||||
Запишите меня! |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* 8. Примеры кода */} |
|
||||||
<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} /> |
|
||||||
<CodeCopy lang="CSS с сайта (perm.oclinica.ru)" code={codeSiteCSS} /> |
|
||||||
</div> |
|
||||||
</Section> |
|
||||||
|
|
||||||
{/* LLM-блок */} |
|
||||||
<LlmBlock path="/components/forms" version="v2.0" specText={LLM_FORMS_TEXT}> |
|
||||||
<LlmSection title="Элементы ввода" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Элемент", "CSS класс", "Тег", "Высота", "Описание"]} |
|
||||||
rows={[ |
|
||||||
["Input", ".bb-input", "<input>", "50px", "Текстовое поле, email, password · как на сайте"], |
|
||||||
["Textarea", ".bb-textarea", "<textarea>", "≥100px", "Многострочный ввод, resize:vertical"], |
|
||||||
["Select", ".bb-select", "<select>", "50px", "Выбор из списка, кастомная стрелка · как на сайте"], |
|
||||||
["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: 1px solid #ccc · border-radius: 4px"], |
|
||||||
["focus", "border: #7ecfca + box-shadow: 0 0 0 3px rgba(126,207,202,0.2)"], |
|
||||||
["error", "border: #dc2626 (+ класс .bb-error)"], |
|
||||||
["disabled", "opacity: 0.5 + cursor: not-allowed + bg: #f8f9fa"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Параметры Toggle" /> |
|
||||||
<LlmTable |
|
||||||
headers={["Параметр", "Тип", "По умолч.", "Описание"]} |
|
||||||
rows={[ |
|
||||||
["defaultChecked", "boolean", "false", "Начальное состояние"], |
|
||||||
["disabled", "boolean", "false", "Блокирует взаимодействие"], |
|
||||||
["label", "string", "—", "Текстовая метка справа от тумблера"], |
|
||||||
["onChange", "(checked: boolean) => void", "—", "Колбэк при изменении"], |
|
||||||
]} |
|
||||||
/> |
|
||||||
<LlmSection title="Правила применения" /> |
|
||||||
<LlmRules |
|
||||||
rules={[ |
|
||||||
{ ok: true, text: "Метка (label) всегда над полем, font-weight: 500" }, |
|
||||||
{ ok: true, text: "Обязательные поля помечены * красным (#dc2626)" }, |
|
||||||
{ ok: true, text: "Hint-текст серым под полем (font-size: 12px)" }, |
|
||||||
{ ok: true, text: "Ошибка — красный текст вместо hint" }, |
|
||||||
{ ok: true, text: "Checkbox-группы — вертикальный список с gap: 10px" }, |
|
||||||
{ ok: true, text: "Toggle — для булевых настроек включить/выключить" }, |
|
||||||
{ ok: false, text: "Не использовать placeholder вместо label" }, |
|
||||||
{ ok: false, text: "Не скрывать признак обязательности поля" }, |
|
||||||
]} |
|
||||||
/> |
|
||||||
</LlmBlock> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,270 +0,0 @@ |
|||||||
"use client"; |
|
||||||
|
|
||||||
import { useState, useEffect } from "react"; |
|
||||||
import { NavigationBlock } from "@/components/blocks/NavigationBlock"; |
|
||||||
import { HeroBlock } from "@/components/blocks/HeroBlock"; |
|
||||||
import { DoctorsBlock } from "@/components/blocks/DoctorsBlock"; |
|
||||||
import { CeoBlock } from "@/components/blocks/CeoBlock"; |
|
||||||
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock"; |
|
||||||
import { NewsBlock } from "@/components/blocks/NewsBlock"; |
|
||||||
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock"; |
|
||||||
import { FooterBlock } from "@/components/blocks/FooterBlock"; |
|
||||||
|
|
||||||
const STORAGE_KEY = "bb-preview-created"; |
|
||||||
const LS_PREFIX = "bb-block-preview:"; |
|
||||||
|
|
||||||
function BlockPlaceholder({ name, href }: { name: string; href: string }) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="w-full py-14 flex flex-col items-center justify-center rounded-xl gap-2" |
|
||||||
style={{ |
|
||||||
border: "2px dashed #d1d5db", |
|
||||||
background: "#f9fafb", |
|
||||||
}} |
|
||||||
> |
|
||||||
<p className="text-sm font-medium" style={{ color: "#6b7280" }}> |
|
||||||
{name} |
|
||||||
</p> |
|
||||||
<a |
|
||||||
href={href} |
|
||||||
className="text-xs underline" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Открыть в брендбуке → |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
interface BlockDef { |
|
||||||
id: string; |
|
||||||
name: string; |
|
||||||
href: string; |
|
||||||
path: string; |
|
||||||
defaultReady: boolean; |
|
||||||
component?: React.ReactNode; |
|
||||||
} |
|
||||||
|
|
||||||
const BLOCKS: BlockDef[] = [ |
|
||||||
{ |
|
||||||
id: "navigation", |
|
||||||
name: "Шапка / Навигация", |
|
||||||
href: "/blocks/navigation", |
|
||||||
path: "/blocks/navigation", |
|
||||||
defaultReady: true, |
|
||||||
component: <NavigationBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "hero", |
|
||||||
name: "Hero-баннер", |
|
||||||
href: "/blocks/hero", |
|
||||||
path: "/blocks/hero", |
|
||||||
defaultReady: true, |
|
||||||
component: <HeroBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "ceo", |
|
||||||
name: "Вводный текст (CEO-блок)", |
|
||||||
href: "/blocks/ceo", |
|
||||||
path: "/blocks/ceo", |
|
||||||
defaultReady: false, |
|
||||||
component: <CeoBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "doctors", |
|
||||||
name: "Наши врачи", |
|
||||||
href: "/blocks/doctors", |
|
||||||
path: "/blocks/doctors", |
|
||||||
defaultReady: true, |
|
||||||
component: <DoctorsBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "reviews", |
|
||||||
name: "Отзывы", |
|
||||||
href: "/blocks/reviews", |
|
||||||
path: "/blocks/reviews", |
|
||||||
defaultReady: false, |
|
||||||
component: <ReviewsBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "contact-forms", |
|
||||||
name: "Формы записи", |
|
||||||
href: "/blocks/contact-forms", |
|
||||||
path: "/blocks/contact-forms", |
|
||||||
defaultReady: false, |
|
||||||
component: <ContactFormsBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "news", |
|
||||||
name: "Новости", |
|
||||||
href: "/blocks/news", |
|
||||||
path: "/blocks/news", |
|
||||||
defaultReady: false, |
|
||||||
component: <NewsBlock />, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "footer", |
|
||||||
name: "Подвал / Контакт", |
|
||||||
href: "/blocks/contact", |
|
||||||
path: "/blocks/contact", |
|
||||||
defaultReady: false, |
|
||||||
component: <FooterBlock />, |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export function PreviewClient() { |
|
||||||
const [created, setCreated] = useState(false); |
|
||||||
const [mounted, setMounted] = useState(false); |
|
||||||
// Map of path → isInPreview from API; null = not loaded yet
|
|
||||||
const [apiMeta, setApiMeta] = useState<Record<string, boolean> | null>(null); |
|
||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL; |
|
||||||
|
|
||||||
// Counter to force re-render when localStorage preview toggles change
|
|
||||||
const [, setRefresh] = useState(0); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setMounted(true); |
|
||||||
if (localStorage.getItem(STORAGE_KEY) === "true") { |
|
||||||
setCreated(true); |
|
||||||
} |
|
||||||
const handler = () => setRefresh((n) => n + 1); |
|
||||||
window.addEventListener("bb-preview-change", handler); |
|
||||||
return () => window.removeEventListener("bb-preview-change", handler); |
|
||||||
}, []); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!apiUrl) return; |
|
||||||
fetch(`${apiUrl}/blocks`) |
|
||||||
.then((r) => r.json()) |
|
||||||
.then((data: Array<{ path: string; isInPreview: boolean }>) => { |
|
||||||
const map: Record<string, boolean> = {}; |
|
||||||
for (const b of data) map[b.path] = b.isInPreview; |
|
||||||
setApiMeta(map); |
|
||||||
}) |
|
||||||
.catch(() => { |
|
||||||
// API offline — fall back to defaultReady
|
|
||||||
}); |
|
||||||
}, [apiUrl]); |
|
||||||
|
|
||||||
function isReady(block: BlockDef): boolean { |
|
||||||
// 1. API online → use API data
|
|
||||||
if (apiMeta !== null && block.path in apiMeta) { |
|
||||||
return apiMeta[block.path] && !!block.component; |
|
||||||
} |
|
||||||
// 2. Check localStorage (set by BlockMetaBar toggle)
|
|
||||||
const lsVal = typeof window !== "undefined" ? localStorage.getItem(LS_PREFIX + block.path) : null; |
|
||||||
if (lsVal !== null) { |
|
||||||
return lsVal === "true" && !!block.component; |
|
||||||
} |
|
||||||
// 3. Fallback to defaultReady
|
|
||||||
return block.defaultReady && !!block.component; |
|
||||||
} |
|
||||||
|
|
||||||
function handleCreate() { |
|
||||||
localStorage.setItem(STORAGE_KEY, "true"); |
|
||||||
setCreated(true); |
|
||||||
} |
|
||||||
|
|
||||||
function handleRebuild() { |
|
||||||
localStorage.removeItem(STORAGE_KEY); |
|
||||||
setCreated(false); |
|
||||||
} |
|
||||||
|
|
||||||
if (!mounted) return null; |
|
||||||
|
|
||||||
const readyCount = BLOCKS.filter(isReady).length; |
|
||||||
|
|
||||||
/* ── ПУСТОЕ СОСТОЯНИЕ ── */ |
|
||||||
if (!created) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="flex flex-col items-center justify-center min-h-screen" |
|
||||||
style={{ background: "var(--bb-bg)" }} |
|
||||||
> |
|
||||||
<div className="text-center max-w-md space-y-5 p-8"> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Страницы |
|
||||||
</p> |
|
||||||
<h1 className="text-2xl font-bold" style={{ color: "var(--bb-text)" }}> |
|
||||||
Просмотр текущей страницы |
|
||||||
</h1> |
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)", lineHeight: 1.7 }}> |
|
||||||
Нажмите «Создать», чтобы собрать главную страницу{" "} |
|
||||||
<span className="font-mono text-xs">perm.oclinica.ru/lor</span> из блоков, |
|
||||||
задокументированных в брендбуке. |
|
||||||
</p> |
|
||||||
<div |
|
||||||
className="flex items-center justify-center gap-2 text-xs px-4 py-2 rounded-lg" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<span |
|
||||||
className="inline-block w-2 h-2 rounded-full" |
|
||||||
style={{ background: "#22c55e" }} |
|
||||||
/> |
|
||||||
<span style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Готово блоков: {readyCount} из {BLOCKS.length} |
|
||||||
</span> |
|
||||||
<span style={{ color: "var(--bb-text-muted)" }}>·</span> |
|
||||||
<span style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{BLOCKS.length - readyCount} плейсхолдеров |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
<button onClick={handleCreate} className="bb-btn bb-btn-md bb-btn-primary"> |
|
||||||
Создать |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/* ── СОЗДАННОЕ СОСТОЯНИЕ ── */ |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
{/* Топ-бар */} |
|
||||||
<div |
|
||||||
className="sticky top-0 z-10 flex items-center justify-between px-6 py-3 border-b" |
|
||||||
style={{ |
|
||||||
background: "var(--bb-sidebar-bg)", |
|
||||||
borderColor: "var(--bb-border)", |
|
||||||
}} |
|
||||||
> |
|
||||||
<div> |
|
||||||
<p |
|
||||||
className="text-xs font-semibold uppercase tracking-widest" |
|
||||||
style={{ color: "var(--brand-053m)" }} |
|
||||||
> |
|
||||||
Просмотр текущей страницы |
|
||||||
</p> |
|
||||||
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
perm.oclinica.ru/lor · {readyCount}/{BLOCKS.length} блоков готово |
|
||||||
{apiMeta === null && apiUrl && ( |
|
||||||
<span style={{ color: "#f59e0b" }}> · API загружается...</span> |
|
||||||
)} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
<button onClick={handleRebuild} className="bb-btn bb-btn-sm bb-btn-outline"> |
|
||||||
Пересобрать |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Собранная страница */} |
|
||||||
<div className="max-w-5xl mx-auto px-8 py-8 space-y-12"> |
|
||||||
{BLOCKS.map((block) => |
|
||||||
isReady(block) ? ( |
|
||||||
<section key={block.id}>{block.component}</section> |
|
||||||
) : ( |
|
||||||
<section key={block.id}> |
|
||||||
<BlockPlaceholder name={block.name} href={block.href} /> |
|
||||||
</section> |
|
||||||
) |
|
||||||
)} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,11 +0,0 @@ |
|||||||
import type { Metadata } from "next"; |
|
||||||
import { PreviewClient } from "./PreviewClient"; |
|
||||||
|
|
||||||
export const metadata: Metadata = { |
|
||||||
title: |
|
||||||
"Просмотр текущей страницы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
|
||||||
}; |
|
||||||
|
|
||||||
export default function PreviewPage() { |
|
||||||
return <PreviewClient />; |
|
||||||
} |
|
||||||
@ -1,45 +0,0 @@ |
|||||||
export const CEO_QUESTIONS = [ |
|
||||||
"У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?", |
|
||||||
"Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?", |
|
||||||
"Заболел ребёнок?", |
|
||||||
"Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых с удобным режимом работы с 9:00 до 21:00 по будням?", |
|
||||||
"Вам назначили проведение эндоскопической операции на ухе, горле или носе?", |
|
||||||
]; |
|
||||||
|
|
||||||
export function CeoBlock() { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
style={{ |
|
||||||
background: "#fff", |
|
||||||
fontFamily: "var(--font-web, 'Fira Sans', sans-serif)", |
|
||||||
fontSize: 14, |
|
||||||
lineHeight: 1.75, |
|
||||||
color: "#374151", |
|
||||||
padding: "40px 48px", |
|
||||||
}} |
|
||||||
> |
|
||||||
<p> |
|
||||||
Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей |
|
||||||
с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах:{" "} |
|
||||||
<a href="#" style={{ color: "#0089c3" }}>ул. Цитная, 9</a>,{" "} |
|
||||||
<a href="#" style={{ color: "#0089c3" }}>ул. Г. Звезда, 31а</a>.{" "} |
|
||||||
Это <a href="#" style={{ color: "#0089c3" }}>Клиника лечения кашля и аллергии</a>. |
|
||||||
</p> |
|
||||||
|
|
||||||
<div style={{ marginTop: 16 }}> |
|
||||||
{CEO_QUESTIONS.map((q) => ( |
|
||||||
<p key={q} style={{ marginBottom: 8 }}>— {q}</p> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
|
|
||||||
<p style={{ marginTop: 16 }}> |
|
||||||
Обращайтесь в ЛОР центр ухо, горло, нос в Перми, наши врачи оториноларингологи обязательно Вам помогут! |
|
||||||
</p> |
|
||||||
<p style={{ marginTop: 8 }}> |
|
||||||
Клиника ЛОР болезней ухо, горло, нос – это наиболее современный центр оториноларингологии в Перми, |
|
||||||
благодаря эндоскопическому оборудованию, высокому профессионализму оториноларингологов. |
|
||||||
</p> |
|
||||||
<p style={{ marginTop: 16, fontWeight: 500 }}>Будьте здоровы!</p> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,84 +0,0 @@ |
|||||||
export function ContactFormsBlock() { |
|
||||||
return ( |
|
||||||
<div className="space-y-8"> |
|
||||||
{/* Форма 1: Будьте здоровы */} |
|
||||||
<div |
|
||||||
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6" |
|
||||||
style={{ background: "#d4f6f8" }} |
|
||||||
> |
|
||||||
<div className="text-center"> |
|
||||||
<h2 className="font-bold mb-1" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}> |
|
||||||
Будьте здоровы! |
|
||||||
</h2> |
|
||||||
<p className="text-sm" style={{ color: "#374151" }}> |
|
||||||
Запишитесь на приём к врачу! |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
<div className="w-full max-w-sm space-y-3"> |
|
||||||
<input |
|
||||||
className="bb-input" |
|
||||||
type="text" |
|
||||||
placeholder="Введите ваше имя" |
|
||||||
readOnly |
|
||||||
/> |
|
||||||
<input |
|
||||||
className="bb-input" |
|
||||||
type="tel" |
|
||||||
placeholder="Введите ваш телефон" |
|
||||||
readOnly |
|
||||||
/> |
|
||||||
<select className="bb-select" disabled> |
|
||||||
<option>Выберите ЛОР врача</option> |
|
||||||
</select> |
|
||||||
<label className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: "#374151" }}> |
|
||||||
<input type="checkbox" className="bb-checkbox mt-0.5" readOnly /> |
|
||||||
<span> |
|
||||||
Отправляя данные, я даю согласие на обработку персональных данных |
|
||||||
</span> |
|
||||||
</label> |
|
||||||
<button className="bb-btn bb-btn-md bb-btn-primary w-full"> |
|
||||||
Запишите меня! |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Форма 2: Узнайте стоимость */} |
|
||||||
<div |
|
||||||
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6" |
|
||||||
style={{ background: "#d4f6f8" }} |
|
||||||
> |
|
||||||
<div className="text-center"> |
|
||||||
<h2 className="font-bold mb-1" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}> |
|
||||||
Узнайте стоимость операции |
|
||||||
</h2> |
|
||||||
<p className="text-sm" style={{ color: "#374151" }}> |
|
||||||
Проконсультируйтесь с ассистентом хирурга |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
<div className="w-full max-w-sm space-y-3"> |
|
||||||
<input |
|
||||||
className="bb-input" |
|
||||||
type="text" |
|
||||||
placeholder="Введите ваше имя" |
|
||||||
readOnly |
|
||||||
/> |
|
||||||
<input |
|
||||||
className="bb-input" |
|
||||||
type="tel" |
|
||||||
placeholder="Введите ваш телефон" |
|
||||||
readOnly |
|
||||||
/> |
|
||||||
<label className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: "#374151" }}> |
|
||||||
<input type="checkbox" className="bb-checkbox mt-0.5" readOnly /> |
|
||||||
<span> |
|
||||||
Отправляя данные, я даю согласие на обработку персональных данных |
|
||||||
</span> |
|
||||||
</label> |
|
||||||
<button className="bb-btn bb-btn-md bb-btn-primary w-full"> |
|
||||||
Перезвоните мне |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,95 +0,0 @@ |
|||||||
export const STATS = [ |
|
||||||
{ num: "27", label: "ЛОР врачей работает в клинике", prefix: "Ежедневно" }, |
|
||||||
{ num: "6", label: "кандидатов медицинских наук", prefix: "В том числе" }, |
|
||||||
{ num: "12 000+", label: "успешно проведённых операций", prefix: "Свыше" }, |
|
||||||
]; |
|
||||||
|
|
||||||
export const DOCTORS = [ |
|
||||||
{ |
|
||||||
name: "Макарова Людмила Германовна", |
|
||||||
spec: "ЛОР врач, сурдолог", |
|
||||||
photo: "/doctors/makarova.jpg", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Семерикова Наталия Александровна", |
|
||||||
spec: "ЛОР врач, сурдолог, хирург. К.М.Н. Завед. Центром сурдологии", |
|
||||||
photo: "/doctors/semerikova.png", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Ворончихина Наталия Валерьевна", |
|
||||||
spec: "Отоневролог, хирург. К.М.Н., доцент кафедры ПГМУ", |
|
||||||
photo: "/doctors/voronchikhina.png", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Лобанова Ирина Юрьевна", |
|
||||||
spec: "ЛОР врач, сурдолог", |
|
||||||
photo: "/doctors/lobanova.jpg", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Торсунова Наталья Сергеевна", |
|
||||||
spec: "Специалист по слухопротезированию (сурдоакустик)", |
|
||||||
photo: "/doctors/torsunova.jpg", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Суворова Светлана Викторовна", |
|
||||||
spec: "ЛОР врач, сурдолог", |
|
||||||
photo: "/doctors/suvorova.jpg", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export function DoctorsBlock() { |
|
||||||
return ( |
|
||||||
<div className="space-y-8"> |
|
||||||
{/* Заголовок + описание */} |
|
||||||
<div> |
|
||||||
<h2 className="font-bold mb-3" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}> |
|
||||||
Приём ведут опытные ЛОР врачи |
|
||||||
</h2> |
|
||||||
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}> |
|
||||||
Фундаментальная теоретическая подготовка и большой практический опыт в сочетании |
|
||||||
с внимательным индивидуальным подходом являются причиной успеха лечения тысяч наших пациентов |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Статистика — без фона, border-bottom #60959c */} |
|
||||||
<div className="grid grid-cols-3 gap-6"> |
|
||||||
{STATS.map((s) => ( |
|
||||||
<div key={s.num} className="pb-3" style={{ borderBottom: "3px solid #60959c" }}> |
|
||||||
<p className="text-lg font-bold leading-snug" style={{ color: "#60959c" }}> |
|
||||||
{s.prefix} {s.num} {s.label} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Сетка врачей — 6 колонок */} |
|
||||||
<div className="grid grid-cols-6 gap-3"> |
|
||||||
{DOCTORS.map((doc) => ( |
|
||||||
<div key={doc.name} className="flex flex-col items-center text-center gap-1.5"> |
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */} |
|
||||||
<img |
|
||||||
src={doc.photo} |
|
||||||
alt={doc.name} |
|
||||||
style={{ |
|
||||||
width: 110, |
|
||||||
height: 150, |
|
||||||
objectFit: "cover", |
|
||||||
objectPosition: "center top", |
|
||||||
borderRadius: 4, |
|
||||||
display: "block", |
|
||||||
}} |
|
||||||
/> |
|
||||||
<div> |
|
||||||
<p className="text-xs font-medium leading-snug" style={{ color: "#60959c" }}> |
|
||||||
{doc.name} |
|
||||||
</p> |
|
||||||
<p className="text-[11px] mt-0.5 leading-snug" style={{ color: "#374151" }}> |
|
||||||
{doc.spec} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,119 +0,0 @@ |
|||||||
const FOOTER_COLUMNS = [ |
|
||||||
{ |
|
||||||
title: "О клинике", |
|
||||||
links: ["Лицензия", "Миссия", "Врачи", "Вакансии", "История", "Образовательная деятельность", "При инфо"], |
|
||||||
}, |
|
||||||
{ |
|
||||||
title: "Заболевания", |
|
||||||
links: ["Ринит", "Отит", "Гайморит", "Тонзиллит", "Полипы носа", "Искривление перегородки"], |
|
||||||
}, |
|
||||||
{ |
|
||||||
title: "Вопрос-ответ", |
|
||||||
links: [ |
|
||||||
"Что нужно знать до операции на ухо", |
|
||||||
"Что нужно знать до операции на нос", |
|
||||||
"Отзывы до и после лечения у детей", |
|
||||||
"Что нужно знать при лечении у детей", |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
title: "Операции", |
|
||||||
links: ["Септопластика", "Турбинопластика", "Тонзиллэктомия", "Аденотомия", "Тимпанопластика", "Мирингопластика"], |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export function FooterBlock() { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="rounded-xl overflow-hidden" |
|
||||||
style={{ border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
{/* 4 колонки ссылок */} |
|
||||||
<div |
|
||||||
className="grid grid-cols-4 gap-6 p-8" |
|
||||||
style={{ background: "#f8f9fa", borderBottom: "1px solid #e5e7eb" }} |
|
||||||
> |
|
||||||
{FOOTER_COLUMNS.map((col) => ( |
|
||||||
<div key={col.title}> |
|
||||||
<p className="font-semibold text-sm mb-3" style={{ color: "#111827" }}> |
|
||||||
{col.title} |
|
||||||
</p> |
|
||||||
<ul className="space-y-1.5"> |
|
||||||
{col.links.map((link) => ( |
|
||||||
<li key={link}> |
|
||||||
<a |
|
||||||
href="#" |
|
||||||
className="text-xs bb-nav-link" |
|
||||||
style={{ color: "#53514e", textDecoration: "none" }} |
|
||||||
> |
|
||||||
{link} |
|
||||||
</a> |
|
||||||
</li> |
|
||||||
))} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Нижняя полоса */} |
|
||||||
<div |
|
||||||
className="flex items-start justify-between gap-6 px-8 py-5" |
|
||||||
style={{ background: "#fff" }} |
|
||||||
> |
|
||||||
{/* Логотип */} |
|
||||||
<div className="flex items-center gap-2 shrink-0"> |
|
||||||
<div |
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold" |
|
||||||
style={{ background: "#0089c3" }} |
|
||||||
> |
|
||||||
✚ |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
<div |
|
||||||
className="text-[10px] font-bold uppercase leading-tight" |
|
||||||
style={{ color: "#53514e" }} |
|
||||||
> |
|
||||||
Клиника<br />ухо, горло, нос |
|
||||||
</div> |
|
||||||
<div className="text-[8px] leading-tight" style={{ color: "#9ca3af" }}> |
|
||||||
им. проф. Е.Н.Оленевой |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Адрес и соцсети */} |
|
||||||
<div className="text-center space-y-2"> |
|
||||||
<p className="text-xs" style={{ color: "#374151" }}> |
|
||||||
Мы находимся по адресу: Пермь, ул. Г. Звезда, 31а |
|
||||||
</p> |
|
||||||
<div className="flex items-center justify-center gap-2"> |
|
||||||
{["VK", "OK", "YT", "TG"].map((s) => ( |
|
||||||
<a |
|
||||||
key={s} |
|
||||||
href="#" |
|
||||||
className="text-[11px] px-2 py-1 rounded" |
|
||||||
style={{ |
|
||||||
background: "var(--bb-sidebar-bg)", |
|
||||||
border: "1px solid var(--bb-border)", |
|
||||||
color: "var(--bb-text-muted)", |
|
||||||
}} |
|
||||||
> |
|
||||||
{s} |
|
||||||
</a> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Часы работы */} |
|
||||||
<div className="text-right text-xs space-y-1" style={{ color: "#374151" }}> |
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "#111827" }}> |
|
||||||
Часы работы: |
|
||||||
</p> |
|
||||||
<p>Пн–пт: 9:00–21:00</p> |
|
||||||
<p>Сб: 9:00–18:00</p> |
|
||||||
<p>Вс: выходной</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,101 +0,0 @@ |
|||||||
export const HERO_CHECKS = [ |
|
||||||
{ key: "БЕЗОПАСНО", desc: "оперируют хирурги с 15-летним опытом работы" }, |
|
||||||
{ key: "БЕЗ ВНЕШНИХ РАЗРЕЗОВ", desc: "хирургия сверхмалых размеров" }, |
|
||||||
{ key: "БЫСТРО", desc: "под наблюдением врача пациент находится 1 сутки" }, |
|
||||||
]; |
|
||||||
|
|
||||||
export function HeroBlock() { |
|
||||||
return ( |
|
||||||
<div className="space-y-3"> |
|
||||||
{/* H1 страницы */} |
|
||||||
<h2 |
|
||||||
className="font-bold" |
|
||||||
style={{ color: "#cb9768", fontSize: 36, lineHeight: "51px" }} |
|
||||||
> |
|
||||||
ЛОР Клиника ухо, горло, нос - медицинский центр лечения ЛОР заболеваний у детей и взрослых |
|
||||||
</h2> |
|
||||||
|
|
||||||
{/* Баннер — единый светло-кремовый фон */} |
|
||||||
<div |
|
||||||
className="rounded-xl overflow-hidden flex" |
|
||||||
style={{ background: "#f9f4e7", minHeight: 280 }} |
|
||||||
> |
|
||||||
{/* Левая часть — контент */} |
|
||||||
<div |
|
||||||
className="flex flex-col justify-center gap-5 p-8" |
|
||||||
style={{ width: "50%", flexShrink: 0 }} |
|
||||||
> |
|
||||||
<p |
|
||||||
className="font-bold uppercase leading-snug" |
|
||||||
style={{ color: "#333333", fontSize: 22 }} |
|
||||||
> |
|
||||||
Эндоскопическое хирургическое лечение ЛОР органов |
|
||||||
</p> |
|
||||||
<ul className="space-y-3"> |
|
||||||
{HERO_CHECKS.map((c) => ( |
|
||||||
<li key={c.key} className="flex items-start gap-2 text-sm"> |
|
||||||
<span |
|
||||||
className="shrink-0 font-bold" |
|
||||||
style={{ color: "#bf9975", marginTop: 1 }} |
|
||||||
> |
|
||||||
✓ |
|
||||||
</span> |
|
||||||
<span> |
|
||||||
<span className="font-bold uppercase" style={{ color: "#111827" }}> |
|
||||||
{c.key} |
|
||||||
</span>{" "} |
|
||||||
<span style={{ color: "#374151" }}>– {c.desc}</span> |
|
||||||
</span> |
|
||||||
</li> |
|
||||||
))} |
|
||||||
</ul> |
|
||||||
<div> |
|
||||||
<button className="bb-btn bb-btn-md bb-btn-pill"> |
|
||||||
Узнать стоимость операции |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Правая часть — фото врача */} |
|
||||||
<div className="flex-1 relative overflow-hidden" style={{ minHeight: 280 }}> |
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */} |
|
||||||
<img |
|
||||||
src="/hero-doctor.jpg" |
|
||||||
alt="Врач на приёме с пациентом" |
|
||||||
style={{ |
|
||||||
width: "100%", |
|
||||||
height: "100%", |
|
||||||
objectFit: "cover", |
|
||||||
objectPosition: "center top", |
|
||||||
position: "absolute", |
|
||||||
inset: 0, |
|
||||||
}} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Под баннером: соцсети + просмотры */} |
|
||||||
<div className="flex items-center gap-3 pt-1"> |
|
||||||
<span className="text-xs" style={{ color: "#9ca3af" }}> |
|
||||||
Поделиться: |
|
||||||
</span> |
|
||||||
{["VK", "FB", "TW"].map((s) => ( |
|
||||||
<button |
|
||||||
key={s} |
|
||||||
className="text-xs px-2 py-1 rounded" |
|
||||||
style={{ |
|
||||||
background: "#f9fafb", |
|
||||||
border: "1px solid #e5e7eb", |
|
||||||
color: "#9ca3af", |
|
||||||
}} |
|
||||||
> |
|
||||||
{s} |
|
||||||
</button> |
|
||||||
))} |
|
||||||
<span className="text-xs ml-2" style={{ color: "#9ca3af" }}> |
|
||||||
👁 98 573 просмотра |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,83 +0,0 @@ |
|||||||
export const NAV_ITEMS = [ |
|
||||||
"Клиника", |
|
||||||
"ЛОР врачи", |
|
||||||
"Заболевания", |
|
||||||
"Вопрос-ответ", |
|
||||||
"ЛОР-операции", |
|
||||||
"Сурдология", |
|
||||||
"Цены", |
|
||||||
"Контакты", |
|
||||||
]; |
|
||||||
|
|
||||||
export function NavigationBlock() { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="overflow-hidden" |
|
||||||
style={{ border: "1px solid #e5e7eb", boxShadow: "0 1px 4px rgba(0,0,0,0.06)" }} |
|
||||||
> |
|
||||||
{/* Топ-бар */} |
|
||||||
<div |
|
||||||
className="flex items-center justify-between px-6 py-2 text-xs border-b" |
|
||||||
style={{ background: "#fff", borderColor: "#e5e7eb", color: "#6b7280" }} |
|
||||||
> |
|
||||||
<div className="flex items-center gap-4"> |
|
||||||
<span>📍 Б. Цитная, 3</span> |
|
||||||
<a href="#" style={{ color: "#0089c3" }}> |
|
||||||
Клиника ухо горло нос и аллергия |
|
||||||
</a> |
|
||||||
<a href="#" style={{ color: "#0089c3" }}> |
|
||||||
Центр аллергологии и пульмонологии |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
<div className="flex items-center gap-4"> |
|
||||||
<span className="font-bold text-sm" style={{ color: "#111827" }}> |
|
||||||
/342/ 255-53-84 |
|
||||||
</span> |
|
||||||
<button className="bb-btn bb-btn-sm bb-btn-pill">Заказать звонок</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Логотип */} |
|
||||||
<div className="flex items-center px-6 py-3 bg-white"> |
|
||||||
<div className="flex items-center gap-3"> |
|
||||||
<div |
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-lg shrink-0" |
|
||||||
style={{ background: "#0089c3" }} |
|
||||||
> |
|
||||||
✚ |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
<div |
|
||||||
className="text-[11px] font-bold leading-tight uppercase tracking-wide" |
|
||||||
style={{ color: "#53514e" }} |
|
||||||
> |
|
||||||
Клиника<br />ухо, горло, нос |
|
||||||
</div> |
|
||||||
<div className="text-[9px] leading-tight mt-0.5" style={{ color: "#9ca3af" }}> |
|
||||||
им. проф. Е.Н.Оленевой |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Главное меню */} |
|
||||||
<nav className="flex border-t bg-white" style={{ borderColor: "#e5e7eb" }}> |
|
||||||
{NAV_ITEMS.map((item, i) => ( |
|
||||||
<a |
|
||||||
key={item} |
|
||||||
href="#" |
|
||||||
className="px-4 py-3 text-sm whitespace-nowrap" |
|
||||||
style={{ |
|
||||||
color: i === 0 ? "#0089c3" : "#53514e", |
|
||||||
borderRight: "1px solid #f3f4f6", |
|
||||||
fontWeight: i === 0 ? 500 : 400, |
|
||||||
textDecoration: "none", |
|
||||||
}} |
|
||||||
> |
|
||||||
{item} |
|
||||||
</a> |
|
||||||
))} |
|
||||||
</nav> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,63 +0,0 @@ |
|||||||
const MOCK_NEWS = [ |
|
||||||
{ |
|
||||||
date: "20.12.2025", |
|
||||||
title: "Наша работа клиники и новогодние праздники", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
{ |
|
||||||
date: "11.08.2025", |
|
||||||
title: "СЕРВИС ОБНОВЛЕНИЕ: Обновление графика работы клиники", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
{ |
|
||||||
date: "12.06.2025", |
|
||||||
title: "СЕРВИС ОБНОВЛЕНИЕ: Временное изменение работы клиники 22.06.25 г.", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
{ |
|
||||||
date: "11.06.2025", |
|
||||||
title: "График работы клиники в ближайшие праздники", |
|
||||||
href: "#", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export function NewsBlock() { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="rounded-xl p-8 space-y-6" |
|
||||||
style={{ background: "#f2fee6" }} |
|
||||||
> |
|
||||||
<h2 className="font-bold" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}> |
|
||||||
Новости |
|
||||||
</h2> |
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> |
|
||||||
{MOCK_NEWS.map((n) => ( |
|
||||||
<div |
|
||||||
key={n.date} |
|
||||||
className="bb-news-card rounded-lg p-3 cursor-pointer" |
|
||||||
style={{ |
|
||||||
border: "1px solid var(--bb-border)", |
|
||||||
transition: "background 0.15s, box-shadow 0.15s", |
|
||||||
}} |
|
||||||
> |
|
||||||
<p className="text-xs mb-2" style={{ color: "#6b7280" }}> |
|
||||||
{n.date} |
|
||||||
</p> |
|
||||||
<a |
|
||||||
href={n.href} |
|
||||||
className="text-sm font-medium leading-snug block" |
|
||||||
style={{ color: "#0089c3", textDecoration: "none" }} |
|
||||||
> |
|
||||||
{n.title} |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
<div className="text-center"> |
|
||||||
<button className="bb-btn bb-btn-md bb-btn-outline"> |
|
||||||
Все новости |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,86 +0,0 @@ |
|||||||
const MOCK_REVIEWS = [ |
|
||||||
{ |
|
||||||
text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.", |
|
||||||
author: "Пациент клиники", |
|
||||||
doctor: "Тимофеева Наталья Александровна", |
|
||||||
}, |
|
||||||
{ |
|
||||||
text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.", |
|
||||||
author: "Наталья К.", |
|
||||||
doctor: "Макарова Людмила Тимофеевна", |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
export function ReviewsBlock() { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="rounded-xl p-8 space-y-6" |
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
{/* Заголовок */} |
|
||||||
<div> |
|
||||||
<h2 className="font-bold mb-2" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}> |
|
||||||
Отзывы о нас |
|
||||||
</h2> |
|
||||||
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}> |
|
||||||
За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа |
|
||||||
более 50 000 пациентов. Но даже сейчас мы высоко ценим каждый положительный отзыв |
|
||||||
и искренние слова благодарности. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Карусель */} |
|
||||||
<div className="relative flex items-center gap-4"> |
|
||||||
{/* Стрелка влево */} |
|
||||||
<button |
|
||||||
className="shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg" |
|
||||||
style={{ |
|
||||||
background: "var(--bb-sidebar-bg)", |
|
||||||
border: "1px solid var(--bb-border)", |
|
||||||
color: "var(--bb-text-muted)", |
|
||||||
}} |
|
||||||
> |
|
||||||
‹ |
|
||||||
</button> |
|
||||||
|
|
||||||
{/* Карточка отзыва */} |
|
||||||
<div |
|
||||||
className="flex-1 rounded-xl p-6" |
|
||||||
style={{ background: "#eef4d1" }} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="text-8xl leading-none mb-2 font-serif" |
|
||||||
style={{ color: "#b8e6ed", lineHeight: 0.8 }} |
|
||||||
> |
|
||||||
« |
|
||||||
</div> |
|
||||||
<p |
|
||||||
className="text-sm italic leading-relaxed" |
|
||||||
style={{ color: "#374151" }} |
|
||||||
> |
|
||||||
{MOCK_REVIEWS[0].text} |
|
||||||
</p> |
|
||||||
<a |
|
||||||
href="#" |
|
||||||
className="inline-block mt-3 text-sm" |
|
||||||
style={{ color: "#0089c3" }} |
|
||||||
> |
|
||||||
Читать отзыв полностью |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Стрелка вправо */} |
|
||||||
<button |
|
||||||
className="shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg" |
|
||||||
style={{ |
|
||||||
background: "var(--bb-sidebar-bg)", |
|
||||||
border: "1px solid var(--bb-border)", |
|
||||||
color: "var(--bb-text-muted)", |
|
||||||
}} |
|
||||||
> |
|
||||||
› |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,175 +0,0 @@ |
|||||||
"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> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,41 +0,0 @@ |
|||||||
"use client"; |
|
||||||
|
|
||||||
export interface ChangelogEntry { |
|
||||||
version: string; |
|
||||||
date: string; |
|
||||||
changes: string[]; |
|
||||||
} |
|
||||||
|
|
||||||
interface BlockChangelogProps { |
|
||||||
changelog: ChangelogEntry[]; |
|
||||||
} |
|
||||||
|
|
||||||
export function BlockChangelog({ changelog }: BlockChangelogProps) { |
|
||||||
if (!changelog || changelog.length === 0) return null; |
|
||||||
|
|
||||||
return ( |
|
||||||
<section className="space-y-3"> |
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
|
||||||
История версий |
|
||||||
</h2> |
|
||||||
<div className="space-y-2 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
{changelog.map((entry) => ( |
|
||||||
<div |
|
||||||
key={entry.version} |
|
||||||
className="p-3 rounded-lg" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}> |
|
||||||
{entry.version} — {entry.date} |
|
||||||
</p> |
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs"> |
|
||||||
{entry.changes.map((change, i) => ( |
|
||||||
<li key={i}>{change}</li> |
|
||||||
))} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,197 +0,0 @@ |
|||||||
"use client"; |
|
||||||
|
|
||||||
import { useState, useEffect } from "react"; |
|
||||||
import { BlockChangelog, type ChangelogEntry } from "./BlockChangelog"; |
|
||||||
|
|
||||||
const LS_PREFIX = "bb-block-preview:"; |
|
||||||
|
|
||||||
function readLocalPreview(path: string, fallback: boolean): boolean { |
|
||||||
if (typeof window === "undefined") return fallback; |
|
||||||
const v = localStorage.getItem(LS_PREFIX + path); |
|
||||||
if (v === null) return fallback; |
|
||||||
return v === "true"; |
|
||||||
} |
|
||||||
|
|
||||||
function writeLocalPreview(path: string, value: boolean) { |
|
||||||
localStorage.setItem(LS_PREFIX + path, String(value)); |
|
||||||
window.dispatchEvent(new Event("bb-preview-change")); |
|
||||||
} |
|
||||||
|
|
||||||
interface BlockMeta { |
|
||||||
path: string; |
|
||||||
name: string; |
|
||||||
version: string; |
|
||||||
isInPreview: boolean; |
|
||||||
changelog: ChangelogEntry[]; |
|
||||||
} |
|
||||||
|
|
||||||
interface BlockMetaBarProps { |
|
||||||
path: string; |
|
||||||
defaultVersion: string; |
|
||||||
defaultIsInPreview: boolean; |
|
||||||
defaultChangelog?: ChangelogEntry[]; |
|
||||||
} |
|
||||||
|
|
||||||
export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, defaultChangelog = [] }: BlockMetaBarProps) { |
|
||||||
const [meta, setMeta] = useState<BlockMeta | null>(null); |
|
||||||
const [editing, setEditing] = useState(false); |
|
||||||
const [versionInput, setVersionInput] = useState(defaultVersion); |
|
||||||
const [saving, setSaving] = useState(false); |
|
||||||
const [togglingPreview, setTogglingPreview] = useState(false); |
|
||||||
const [apiDown, setApiDown] = useState(false); |
|
||||||
const [localPreview, setLocalPreview] = useState(defaultIsInPreview); |
|
||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL; |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setLocalPreview(readLocalPreview(path, defaultIsInPreview)); |
|
||||||
}, [path, defaultIsInPreview]); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!apiUrl) { setApiDown(true); return; } |
|
||||||
fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`) |
|
||||||
.then((r) => r.json()) |
|
||||||
.then((data: BlockMeta) => { |
|
||||||
setMeta(data); |
|
||||||
setVersionInput(data.version); |
|
||||||
}) |
|
||||||
.catch(() => setApiDown(true)); |
|
||||||
}, [apiUrl, path]); |
|
||||||
|
|
||||||
async function patch(data: { version?: string; isInPreview?: boolean; changelog?: ChangelogEntry[] }) { |
|
||||||
if (!apiUrl) return null; |
|
||||||
const r = await fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`, { |
|
||||||
method: 'PATCH', |
|
||||||
headers: { 'Content-Type': 'application/json' }, |
|
||||||
body: JSON.stringify(data), |
|
||||||
}); |
|
||||||
return r.json() as Promise<BlockMeta>; |
|
||||||
} |
|
||||||
|
|
||||||
async function saveVersion() { |
|
||||||
if (!meta) return; |
|
||||||
setSaving(true); |
|
||||||
try { |
|
||||||
const updated = await patch({ version: versionInput }); |
|
||||||
if (updated) { setMeta(updated); setEditing(false); } |
|
||||||
} finally { |
|
||||||
setSaving(false); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function togglePreview() { |
|
||||||
if (apiDown) { |
|
||||||
const newVal = !localPreview; |
|
||||||
setLocalPreview(newVal); |
|
||||||
writeLocalPreview(path, newVal); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (!meta) return; |
|
||||||
setTogglingPreview(true); |
|
||||||
try { |
|
||||||
const updated = await patch({ isInPreview: !meta.isInPreview }); |
|
||||||
if (updated) setMeta(updated); |
|
||||||
} finally { |
|
||||||
setTogglingPreview(false); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const version = meta?.version ?? defaultVersion; |
|
||||||
const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview); |
|
||||||
const changelog: ChangelogEntry[] = (meta?.changelog && meta.changelog.length > 0) ? meta.changelog : defaultChangelog; |
|
||||||
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<div |
|
||||||
className="flex items-center flex-wrap gap-3 px-4 py-2 rounded-lg text-xs mb-6" |
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
||||||
> |
|
||||||
{/* Version badge */} |
|
||||||
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}> |
|
||||||
Версия: |
|
||||||
</span> |
|
||||||
{editing ? ( |
|
||||||
<span className="flex items-center gap-1.5"> |
|
||||||
<input |
|
||||||
className="px-2 py-0.5 rounded border text-xs font-mono w-20" |
|
||||||
style={{ borderColor: "var(--bb-border)", color: "var(--bb-text)" }} |
|
||||||
value={versionInput} |
|
||||||
onChange={(e) => setVersionInput(e.target.value)} |
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }} |
|
||||||
autoFocus |
|
||||||
/> |
|
||||||
<button |
|
||||||
onClick={saveVersion} |
|
||||||
disabled={saving} |
|
||||||
className="px-2 py-0.5 rounded text-xs font-medium" |
|
||||||
style={{ background: "var(--brand-053m)", color: "#fff" }} |
|
||||||
> |
|
||||||
{saving ? '...' : 'Сохранить'} |
|
||||||
</button> |
|
||||||
<button |
|
||||||
onClick={() => { setEditing(false); setVersionInput(version); }} |
|
||||||
className="px-2 py-0.5 rounded text-xs" |
|
||||||
style={{ color: "var(--bb-text-muted)" }} |
|
||||||
> |
|
||||||
Отмена |
|
||||||
</button> |
|
||||||
</span> |
|
||||||
) : ( |
|
||||||
<button |
|
||||||
onClick={() => !apiDown && setEditing(true)} |
|
||||||
title={apiDown ? 'Версия из кода (API недоступен)' : 'Изменить версию'} |
|
||||||
className="font-mono px-2 py-0.5 rounded" |
|
||||||
style={{ |
|
||||||
background: "var(--bb-sidebar-active-bg, #dff0fa)", |
|
||||||
color: "var(--brand-053m)", |
|
||||||
cursor: apiDown ? 'default' : 'pointer', |
|
||||||
}} |
|
||||||
> |
|
||||||
{version} |
|
||||||
</button> |
|
||||||
)} |
|
||||||
|
|
||||||
<span style={{ color: "var(--bb-border)" }}>·</span> |
|
||||||
|
|
||||||
{/* Preview toggle */} |
|
||||||
<button |
|
||||||
onClick={togglePreview} |
|
||||||
disabled={togglingPreview || (!apiDown && !meta)} |
|
||||||
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"} |
|
||||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded font-medium transition-colors cursor-pointer" |
|
||||||
style={isInPreview ? { |
|
||||||
background: "#dcfce7", |
|
||||||
color: "#16a34a", |
|
||||||
border: "1px solid #86efac", |
|
||||||
} : { |
|
||||||
background: "var(--bb-sidebar-bg)", |
|
||||||
color: "var(--bb-text-muted)", |
|
||||||
border: "1px solid var(--bb-border)", |
|
||||||
}} |
|
||||||
> |
|
||||||
<span |
|
||||||
className="inline-block w-1.5 h-1.5 rounded-full" |
|
||||||
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }} |
|
||||||
/> |
|
||||||
{togglingPreview |
|
||||||
? '...' |
|
||||||
: isInPreview |
|
||||||
? "В превью" |
|
||||||
: "Не в превью"} |
|
||||||
</button> |
|
||||||
|
|
||||||
{/* Subtle offline dot instead of "API офлайн" text */} |
|
||||||
{apiDown && ( |
|
||||||
<span |
|
||||||
className="inline-block w-1.5 h-1.5 rounded-full" |
|
||||||
title="API недоступен — данные из кода" |
|
||||||
style={{ background: "#d1d5db" }} |
|
||||||
/> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
|
|
||||||
{/* Changelog rendered from API or fallback */} |
|
||||||
{changelog.length > 0 && <BlockChangelog changelog={changelog} />} |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,45 +0,0 @@ |
|||||||
"use client"; |
|
||||||
|
|
||||||
import { ButtonHTMLAttributes, forwardRef } from "react"; |
|
||||||
|
|
||||||
export type ButtonVariant = "primary" | "outline" | "teal" | "pill"; |
|
||||||
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> |
|
||||||
); |
|
||||||
} |
|
||||||
); |
|
||||||
@ -1,60 +0,0 @@ |
|||||||
"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> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,59 +0,0 @@ |
|||||||
"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> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,14 +1,10 @@ |
|||||||
import type { NextConfig } from "next"; |
import type { NextConfig } from "next"; |
||||||
import path from "path"; |
import path from "path"; |
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === "development"; |
|
||||||
|
|
||||||
const nextConfig: NextConfig = { |
const nextConfig: NextConfig = { |
||||||
...(isDev && { |
|
||||||
turbopack: { |
turbopack: { |
||||||
root: path.resolve(__dirname, "../.."), |
root: path.resolve(__dirname, "../.."), |
||||||
}, |
}, |
||||||
}), |
|
||||||
}; |
}; |
||||||
|
|
||||||
export default nextConfig; |
export default nextConfig; |
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 762 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 871 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 740 B |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 767 B |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 957 B |
|
Before Width: | Height: | Size: 446 B |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 699 B |
|
Before Width: | Height: | Size: 153 B |
|
Before Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 153 B |
|
Before Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 153 B |
|
Before Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
@ -1,156 +0,0 @@ |
|||||||
# Деплой — oclinica-brandbook |
|
||||||
|
|
||||||
## Текущее состояние |
|
||||||
|
|
||||||
| Сервис | Статус | URL | Платформа | |
|
||||||
|------------|-------------|----------------------------------------|---------------| |
|
||||||
| Фронтенд | ✅ Активен | https://web-oclinica.vercel.app | Vercel Hobby | |
|
||||||
| Бэкенд | локально | http://localhost:3001 | Docker Compose | |
|
||||||
| База данных | локально | localhost:5433 | PostgreSQL 16 | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Фронтенд — Vercel |
|
||||||
|
|
||||||
### Первоначальная настройка (уже выполнена) |
|
||||||
|
|
||||||
```bash |
|
||||||
# 1. Установить Vercel CLI |
|
||||||
npm install -g vercel |
|
||||||
|
|
||||||
# 2. Войти в аккаунт (однократно, открывает браузер) |
|
||||||
vercel login |
|
||||||
|
|
||||||
# 3. Первый деплой из директории apps/web |
|
||||||
cd apps/web |
|
||||||
vercel --yes |
|
||||||
``` |
|
||||||
|
|
||||||
### Деплой обновлений |
|
||||||
|
|
||||||
```bash |
|
||||||
cd apps/web |
|
||||||
vercel --prod --yes |
|
||||||
``` |
|
||||||
|
|
||||||
Деплой занимает ~30 секунд. После завершения изменения сразу доступны по адресу: |
|
||||||
**https://web-oclinica.vercel.app** |
|
||||||
|
|
||||||
### Как это работает |
|
||||||
|
|
||||||
- Vercel автоматически определяет Next.js и использует pnpm для сборки |
|
||||||
- Каждый `vercel --prod` создаёт новый immutable deployment и привязывает его к production URL |
|
||||||
- Предыдущие деплои остаются доступны по уникальным preview URL |
|
||||||
- Логи билда: https://vercel.com/oclinica/web |
|
||||||
|
|
||||||
### Ограничения Vercel Hobby (бесплатный план) |
|
||||||
|
|
||||||
| Параметр | Лимит | |
|
||||||
|-----------------------|-------------------------| |
|
||||||
| Bandwidth | 100 GB / месяц | |
|
||||||
| Builds | 6000 минут / месяц | |
|
||||||
| Serverless Functions | 100 GB-hours / месяц | |
|
||||||
| Тип использования | Только некоммерческие | |
|
||||||
|
|
||||||
Для брендбука (внутренний инструмент) лимитов более чем достаточно. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Локальная разработка |
|
||||||
|
|
||||||
### Запуск фронтенда |
|
||||||
|
|
||||||
```bash |
|
||||||
# Из корня monorepo |
|
||||||
pnpm dev |
|
||||||
|
|
||||||
# Или только фронтенд |
|
||||||
cd apps/web && pnpm dev |
|
||||||
``` |
|
||||||
|
|
||||||
Доступен на: http://localhost:3001 |
|
||||||
|
|
||||||
### Запуск бэкенда + БД |
|
||||||
|
|
||||||
```bash |
|
||||||
# Запустить PostgreSQL |
|
||||||
docker compose up -d |
|
||||||
|
|
||||||
# Запустить NestJS |
|
||||||
cd apps/api && pnpm dev |
|
||||||
``` |
|
||||||
|
|
||||||
### .env файлы |
|
||||||
|
|
||||||
```bash |
|
||||||
# Скопировать и заполнить |
|
||||||
cp .env.example .env |
|
||||||
``` |
|
||||||
|
|
||||||
Содержимое `.env.example`: |
|
||||||
``` |
|
||||||
DATABASE_URL="postgresql://brandbook:brandbook@localhost:5433/brandbook" |
|
||||||
API_PORT=3001 |
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:3001 |
|
||||||
``` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Автодеплой через Gitea Actions (планируется в Sprint 12) |
|
||||||
|
|
||||||
Для автоматического деплоя при пуше в ветку `main` создать файл |
|
||||||
`.gitea/workflows/deploy-frontend.yml`: |
|
||||||
|
|
||||||
```yaml |
|
||||||
name: Deploy Frontend to Vercel |
|
||||||
|
|
||||||
on: |
|
||||||
push: |
|
||||||
branches: [main] |
|
||||||
paths: |
|
||||||
- 'apps/web/**' |
|
||||||
|
|
||||||
jobs: |
|
||||||
deploy: |
|
||||||
runs-on: ubuntu-latest |
|
||||||
steps: |
|
||||||
- uses: actions/checkout@v4 |
|
||||||
|
|
||||||
- name: Setup pnpm |
|
||||||
uses: pnpm/action-setup@v4 |
|
||||||
with: |
|
||||||
version: 10 |
|
||||||
|
|
||||||
- name: Setup Node.js |
|
||||||
uses: actions/setup-node@v4 |
|
||||||
with: |
|
||||||
node-version: 20 |
|
||||||
cache: 'pnpm' |
|
||||||
cache-dependency-path: apps/web/pnpm-lock.yaml |
|
||||||
|
|
||||||
- name: Install Vercel CLI |
|
||||||
run: npm install -g vercel |
|
||||||
|
|
||||||
- name: Deploy to Vercel |
|
||||||
run: cd apps/web && vercel --prod --yes --token ${{ secrets.VERCEL_TOKEN }} |
|
||||||
``` |
|
||||||
|
|
||||||
**Настройка:** |
|
||||||
1. Получить Vercel Token: https://vercel.com/account/tokens |
|
||||||
2. Добавить в Gitea: Settings → Secrets → `VERCEL_TOKEN` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Хостинг бэкенда (планируется к Sprint 11) |
|
||||||
|
|
||||||
Бэкенд (NestJS + PostgreSQL) потребуется для экспериментальной секции (Sprint 11). |
|
||||||
Варианты для рассмотрения: |
|
||||||
|
|
||||||
| Платформа | PostgreSQL | Бесплатно | Карта | |
|
||||||
|-----------|-----------|-----------|-------| |
|
||||||
| Railway | ✅ | $5 кредит / месяц | нужна | |
|
||||||
| Supabase | ✅ | ✅ (PostgreSQL managed) | нет | |
|
||||||
| Fly.io | ✅ | ✅ ограниченно | нет | |
|
||||||
| VPS клиники | ✅ | ✅ (если есть) | нет | |
|
||||||
|
|
||||||
Рекомендация: **Supabase** для БД (бесплатно, managed PostgreSQL) + **Railway** или VPS для NestJS. |
|
||||||
@ -1,458 +0,0 @@ |
|||||||
# LLM Context — Цифровой брендбук Клиники |
|
||||||
|
|
||||||
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой |
|
||||||
|
|
||||||
**Версия контекста:** 4.3 |
|
||||||
**Дата обновления:** 2026-03-24 |
|
||||||
**Актуальный спринт:** Sprint 5.5 |
|
||||||
**Сайт клиники:** https://oclinica.ru |
|
||||||
**Брендбук (локально):** http://localhost:3001 |
|
||||||
**Брендбук (production):** https://web-oclinica.vercel.app |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Назначение файла |
|
||||||
|
|
||||||
Этот файл — единый источник дизайн-данных клиники в машиночитаемом формате. |
|
||||||
Используется как контекст для LLM при: |
|
||||||
- разработке страниц и компонентов сайта |
|
||||||
- создании макетов и прототипов |
|
||||||
- разработке мобильных приложений |
|
||||||
- проектировании новых носителей бренда |
|
||||||
- генерации CSS / Tailwind / Figma Tokens |
|
||||||
|
|
||||||
При работе с любым материалом клиники **всегда загружай этот файл первым**. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 1. О клинике |
|
||||||
|
|
||||||
**Полное название:** Клиника ухо, горло, нос им. проф. Е.Н.Оленевой |
|
||||||
**Тип:** Медицинская клиника, отоларингология (ЛОР) |
|
||||||
**Город:** Пермь |
|
||||||
**Платформа сайта:** Drupal (тема `clinic_bootstrap_mobile`) |
|
||||||
**Аудитория:** Пациенты, врачи клиники, подрядчики |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 2. Цветовая палитра |
|
||||||
|
|
||||||
### 2.1 Фирменные цвета (Oracal — базовая палитра) |
|
||||||
|
|
||||||
Основаны на кодах самоклеящейся плёнки Oracal. HEX-значения рассчитаны по **точным RGB |
|
||||||
из каталога Oracal**. Для печати и оффлайн-носителей используй коды Oracal, не HEX. |
|
||||||
|
|
||||||
| Oracal | Название | HEX | RGB | HSL | CSS-переменная | Применение | |
|
||||||
|--------|-----------------------|-----------|-------------------|-------------------|------------------|------------| |
|
||||||
| 053M | Основной бирюзовый | `#0089c3` | rgb(0,137,195) | hsl(198,100%,38%) | `--brand-053m` | Акцент, CTA-кнопки, иконки, активные состояния | |
|
||||||
| 073M | Тёмный серо-голубой | `#53514e` | rgb(83,81,78) | hsl(38,3%,32%) | `--brand-073m` | Тёмный фон, хедер, заголовки навигации | |
|
||||||
| 066M | Средний бирюзовый | `#00818c` | rgb(0,129,140) | hsl(185,100%,27%) | `--brand-066m` | Вторичные акценты, фоны секций, иллюстрации | |
|
||||||
| 050M | Тёмно-синий | `#1b2e5d` | rgb(27,46,93) | hsl(223,55%,24%) | `--brand-050m` | Наружная реклама, полиграфия, заголовки на светлом | |
|
||||||
| 081M | Бежевый | `#a8885c` | rgb(168,136,92) | hsl(35,30%,51%) | `--brand-081m` | Форма сотрудников, оффлайн носители, тёплые акценты | |
|
||||||
| 080M | Тёмно-коричневый | `#432f1e` | rgb(67,47,30) | hsl(27,38%,19%) | `--brand-080m` | Текст на бежевых поверхностях, логотип на форме | |
|
||||||
| — | Белый | `#ffffff` | rgb(255,255,255) | hsl(0,0%,100%) | `--brand-white` | Фон, инвертированный текст, логотип на тёмных фонах | |
|
||||||
|
|
||||||
### 2.2 Цвета сайта oclinica.ru (реальный CSS) |
|
||||||
|
|
||||||
Извлечены из CSS темы Drupal: |
|
||||||
`https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css` |
|
||||||
Метод: python + regex + Counter, 2026-03-22 |
|
||||||
|
|
||||||
| Название | HEX | RGB | Группа | Вхождений | Применение на сайте | |
|
||||||
|-----------------------|-----------|-------------------|----------|-----------|---------------------| |
|
||||||
| Бежевый | `#bf9975` | rgb(191,153,117) | Акценты | 12 | Основной тёплый акцент, фоны, рамки, текст | |
|
||||||
| Серо-бирюзовый | `#60959c` | rgb(96,149,156) | Акценты | 7 | Основной холодный акцент, ссылки | |
|
||||||
| Бирюзовый | `#63bac3` | rgb(99,186,195) | Акценты | 4 | Фоны акцентных блоков, иконки | |
|
||||||
| Бирюзовый средний | `#52b4bd` | rgb(82,180,189) | Акценты | 4 | Вторичные цветовые акценты | |
|
||||||
| Коралловый | `#ffa39c` | rgb(255,163,156) | Акценты | 2 | CTA-кнопки («Запишите меня!») | |
|
||||||
| Основной текст | `#464646` | rgb(70,70,70) | Текст | 3 | Цвет основного текста на сайте | |
|
||||||
| Второстепенный текст | `#949290` | rgb(148,146,144) | Текст | 4 | Подписи, второстепенный контент | |
|
||||||
| H1 страницы | `#cb9768` | rgb(203,151,104) | Текст | 1 | Цвет H1 на страницах разделов | |
|
||||||
| Светло-кремовый Hero | `#f9f4e7` | rgb(249,244,231) | Фоны | 1 | Фон Hero-баннера страниц разделов | |
|
||||||
| Светло-бирюзовый фон | `#d4f6f8` | rgb(212,246,248) | Фоны | 2 | Фон обеих форм записи (ранее #b8e6ed) | |
|
||||||
| Кремовый фон | `#e9e4d4` | rgb(233,228,212) | Фоны | 1 | Тёплые фоны кнопок-пилюль, вторичные секции | |
|
||||||
| Светло-жёлтый фон | `#eef4d1` | rgb(238,244,209) | Фоны | 1 | Фон карточек отзывов | |
|
||||||
| Светло-зелёный фон | `#f2fee6` | rgb(242,254,230) | Фоны | 1 | Фон секции новостей | |
|
||||||
|
|
||||||
### 2.3 Соответствие Oracal → Сайт |
|
||||||
|
|
||||||
| Oracal (бренд) | HEX бренда | Сайт (цифровой) | HEX сайта | Отклонение | |
|
||||||
|--------------------------|------------|-----------------------|-----------|------------| |
|
||||||
| 053M Основной бирюзовый | `#0089c3` | Бирюзовый | `#63bac3` | Oracal ярко-синий; сайт светлее и голубее | |
|
||||||
| 073M Тёмный серо-голубой | `#53514e` | Серо-бирюзовый | `#60959c` | Oracal тёмно-серый; сайт серо-бирюзовый (значительное расхождение) | |
|
||||||
| 066M Средний бирюзовый | `#00818c` | Бирюзовый средний | `#52b4bd` | Oracal тёмный бирюзовый; сайт светлее | |
|
||||||
| 081M Бежевый | `#a8885c` | Бежевый | `#bf9975` | Oracal тёплый охристый; сайт прохладнее и светлее | |
|
||||||
| 050M Тёмно-синий | `#1b2e5d` | — | — | Не найден в CSS сайта | |
|
||||||
| 080M Тёмно-коричневый | `#432f1e` | — | — | Не найден в CSS сайта | |
|
||||||
|
|
||||||
**Важно:** расхождения ожидаемы — это цифровая адаптация плёночных цветов под экран. |
|
||||||
При разработке digital-материалов используй цвета сайта (раздел 2.2), не Oracal. |
|
||||||
|
|
||||||
### 2.4 Контрастность пар (WCAG 2.1) |
|
||||||
|
|
||||||
| Пара | Коэффициент | AA (4.5:1) | AAA (7:1) | AA large (3:1) | |
|
||||||
|----------------------------------------------|-------------|------------|-----------|----------------| |
|
||||||
| Белый на тёмном серо-голубом (#53514e 073M) | 7.9:1 | ✓ | ✓ | ✓ | |
|
||||||
| Белый на тёмно-синем (#1b2e5d 050M) | 13.2:1 | ✓ | ✓ | ✓ | |
|
||||||
| Белый на тёмно-коричневом (#432f1e 080M) | 12.6:1 | ✓ | ✓ | ✓ | |
|
||||||
| Белый на среднем бирюзовом (#00818c 066M) | 4.7:1 | ✓ | ✕ | ✓ | |
|
||||||
| Тёмный (#111827) на основном бирюзовом (#0089c3 053M) | 4.7:1 | ✓ | ✕ | ✓ | |
|
||||||
| Тёмный (#111827) на бежевом (#a8885c 081M) | 5.5:1 | ✓ | ✕ | ✓ | |
|
||||||
| Тёмно-коричневый (#432f1e) на бежевом (#a8885c) | 3.8:1 | ✕ | ✕ | ✓ (только крупный ≥18pt) | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 3. Типографика |
|
||||||
|
|
||||||
### 3.1 DINPro — фирменный шрифт (оффлайн) |
|
||||||
|
|
||||||
**Применение:** логотип, оффлайн-носители (бейджи, навигационные таблички, транспорт, |
|
||||||
форма сотрудников, полиграфия). |
|
||||||
**Не использовать** на веб-сайте и в digital-материалах (лицензионный шрифт, |
|
||||||
нет легального встраивания в веб). |
|
||||||
**Веса:** Regular, Medium, Bold |
|
||||||
|
|
||||||
| Стиль | font-weight | font-size | line-height | letter-spacing | Применение | |
|
||||||
|----------|-------------|-----------|-------------|----------------|------------| |
|
||||||
| Display | 700 | 48px | 1.15 | -0.5px | Крупные заголовки носителей | |
|
||||||
| H1 | 700 | 36px | 1.2 | -0.3px | Основной заголовок печатных материалов | |
|
||||||
| H2 | 700 | 28px | 1.25 | -0.2px | Подзаголовки носителей | |
|
||||||
| H3 | 500 | 22px | 1.3 | 0px | Подзаголовки третьего уровня | |
|
||||||
| Body | 400 | 16px | 1.5 | 0px | Основной текст оффлайн | |
|
||||||
| Small | 400 | 12px | 1.4 | 0.2px | Подписи, бейджи, таблички | |
|
||||||
| Caption | 400 | 10px | 1.3 | 0.4px | Сноски, технические пометки | |
|
||||||
|
|
||||||
### 3.2 Fira Sans — веб-шрифт (digital) |
|
||||||
|
|
||||||
**Применение:** сайт oclinica.ru, цифровые коммуникации, digital-баннеры, email. |
|
||||||
**Источник:** Google Fonts (`https://fonts.google.com/specimen/Fira+Sans`) |
|
||||||
**Поддержка кириллицы:** да |
|
||||||
**Используемые веса:** 300 (Light), 400 (Regular), 500 (Medium), 600 (SemiBold) |
|
||||||
**CSS:** `font-family: 'Fira Sans', sans-serif;` |
|
||||||
|
|
||||||
| Стиль | font-weight | font-size | line-height | letter-spacing | Применение | |
|
||||||
|----------------|-------------|---------------|-------------|----------------|------------| |
|
||||||
| h1 | 600 | 2.25rem (36px)| 1.25 | -0.025em | Заголовок страницы | |
|
||||||
| h2 | 600 | 1.875rem (30px)| 1.3 | -0.02em | Заголовок секции | |
|
||||||
| h3 | 500 | 1.5rem (24px) | 1.375 | -0.01em | Подзаголовок | |
|
||||||
| h4 | 500 | 1.25rem (20px)| 1.4 | 0em | Заголовок компонента | |
|
||||||
| h5 | 500 | 1.125rem (18px)| 1.45 | 0em | Подзаголовок компонента | |
|
||||||
| h6 | 500 | 1rem (16px) | 1.5 | 0.01em | Метка секции | |
|
||||||
| body-large | 400 | 1.125rem (18px)| 1.6 | 0em | Лид-текст, вводный абзац | |
|
||||||
| body | 400 | 1rem (16px) | 1.625 | 0em | Основной текст | |
|
||||||
| body-small | 400 | 0.875rem (14px)| 1.5 | 0em | Дополнительный текст, подписи | |
|
||||||
| caption | 300 | 0.75rem (12px)| 1.4 | 0.02em | Подписи к изображениям, сноски | |
|
||||||
| label | 500 | 0.875rem (14px)| 1.2 | 0.03em | Метки форм | |
|
||||||
| overline | 600 | 0.6875rem (11px)| 1.2 | 0.1em | Надписи над заголовками (uppercase) | |
|
||||||
|
|
||||||
**Принцип выбора шрифта:** |
|
||||||
- Носитель цифровой → Fira Sans |
|
||||||
- Носитель печатный / физический → DINPro |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 4. Логотип |
|
||||||
|
|
||||||
### 4.1 Версии логотипа |
|
||||||
|
|
||||||
**Основной логотип** — включает название специализации («ухо, горло, нос»). |
|
||||||
Используется на всех основных носителях: сайт, навигация, транспорт, форма. |
|
||||||
|
|
||||||
**Общий логотип** — без специализации, только название клиники или сеть. |
|
||||||
Используется для обозначения сети клиник, в корпоративных материалах. |
|
||||||
|
|
||||||
### 4.2 Файлы |
|
||||||
|
|
||||||
| Файл | Описание | Путь в проекте | |
|
||||||
|------|----------|----------------| |
|
||||||
| `logo-transparent.png` | Логотип с прозрачным фоном | `apps/web/public/logo/logo-transparent.png` | |
|
||||||
|
|
||||||
SVG-версия ожидается (не получена от клиники). |
|
||||||
|
|
||||||
### 4.3 Цветовые варианты |
|
||||||
|
|
||||||
| Вариант | Фон | Логотип | Применение | |
|
||||||
|---------|-----|---------|------------| |
|
||||||
| Основной | Белый / светлый | Полноцветный | Сайт, полиграфия на белом | |
|
||||||
| Инвертированный | Тёмный (#53514e, #1b2e5d) | Белый (`filter: brightness(0) invert(1)`) | Хедер, тёмные секции | |
|
||||||
| На форме (беж) | Бежевый (#a8885c / Oracal 081M) | Коричневый (#432f1e / Oracal 080M) | Одежда персонала | |
|
||||||
| На форме (синий) | Тёмно-синий (#1b2e5d / Oracal 050M) | Белый | Одежда персонала | |
|
||||||
|
|
||||||
### 4.4 Охранная зона |
|
||||||
|
|
||||||
Минимальный отступ вокруг логотипа = **высота буквы «К»** в названии. |
|
||||||
Запрещено размещать другие элементы в охранной зоне. |
|
||||||
|
|
||||||
### 4.5 Минимальные размеры |
|
||||||
|
|
||||||
| Носитель | Размер логотипа | |
|
||||||
|----------|----------------| |
|
||||||
| Одежда до р.46 | 70 × 25,5 мм | |
|
||||||
| Одежда от р.48 | 90 × 32,8 мм | |
|
||||||
|
|
||||||
### 4.6 Запрещено |
|
||||||
|
|
||||||
- Искажать пропорции |
|
||||||
- Менять цвета на нефирменные |
|
||||||
- Добавлять тени, обводки, эффекты |
|
||||||
- Размещать на пёстром или плохо контрастном фоне |
|
||||||
- Использовать низкое разрешение (< 150 dpi для печати) |
|
||||||
- Переворачивать или отражать |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 5. CSS-переменные брендбука |
|
||||||
|
|
||||||
Определены в `apps/web/app/globals.css`: |
|
||||||
|
|
||||||
```css |
|
||||||
/* Цвета бренда (точные RGB из каталога Oracal) */ |
|
||||||
--brand-053m: #0089c3; /* Основной бирюзовый · rgb(0,137,195) */ |
|
||||||
--brand-073m: #53514e; /* Тёмный серо-голубой · rgb(83,81,78) */ |
|
||||||
--brand-066m: #00818c; /* Средний бирюзовый · rgb(0,129,140) */ |
|
||||||
--brand-050m: #1b2e5d; /* Тёмно-синий · rgb(27,46,93) */ |
|
||||||
--brand-081m: #a8885c; /* Бежевый · rgb(168,136,92) */ |
|
||||||
--brand-080m: #432f1e; /* Тёмно-коричневый · rgb(67,47,30) */ |
|
||||||
--brand-white: #ffffff; /* Белый */ |
|
||||||
|
|
||||||
/* UI брендбука */ |
|
||||||
--bb-sidebar-bg: #f8f9fa; |
|
||||||
--bb-sidebar-border: #e5e7eb; |
|
||||||
--bb-sidebar-text: #374151; |
|
||||||
--bb-sidebar-text-muted: #6b7280; |
|
||||||
--bb-sidebar-active-bg: #dff0fa; /* светло-синий под 053M */ |
|
||||||
--bb-sidebar-active-text: var(--brand-053m); |
|
||||||
--bb-sidebar-section: #9ca3af; |
|
||||||
--bb-content-bg: #ffffff; |
|
||||||
--bb-border: #e5e7eb; |
|
||||||
--bb-text: #111827; |
|
||||||
--bb-text-muted: #6b7280; |
|
||||||
``` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 6. Оффлайн носители |
|
||||||
|
|
||||||
### 6.1 Форма сотрудников |
|
||||||
|
|
||||||
**Варианты:** |
|
||||||
- Бежевый: ткань цвета Oracal 081M (#a8885c), логотип Oracal 080M (#432f1e, коричневый), расположение — левая сторона груди |
|
||||||
- Синий: ткань цвета Oracal 050M (#1b2e5d, тёмно-синий), логотип белый (Oracal 010), расположение — левая сторона груди |
|
||||||
|
|
||||||
**Размеры логотипа на форме:** |
|
||||||
- Размеры до 46: 70 × 25,5 мм |
|
||||||
- Размеры от 48: 90 × 32,8 мм |
|
||||||
|
|
||||||
### 6.2 Бейджи |
|
||||||
|
|
||||||
**Размер:** 70 × 30 мм |
|
||||||
**Варианты:** серый (светлый) и белый |
|
||||||
**Состав текста:** имя, должность, учёная степень (при наличии) |
|
||||||
**Шрифт:** DINPro |
|
||||||
**Логотип:** левый верхний угол или левая часть |
|
||||||
|
|
||||||
### 6.3 Внутренняя навигация |
|
||||||
|
|
||||||
**Материал:** оргстекло |
|
||||||
**Плёнки:** Oracal 053M (#0089c3) и 073M (#53514e) |
|
||||||
**Типы табличек:** |
|
||||||
- Таблички на дверях кабинетов: номер кабинета, профиль врача с фото и QR-кодом |
|
||||||
- Указатели по этажам: стрелки направлений + номера кабинетов |
|
||||||
|
|
||||||
**Нумерация кабинетов:** |
|
||||||
- Двузначное число, крупный шрифт (DINPro Bold) |
|
||||||
- Фон: бирюзовый (053M), номер: белый |
|
||||||
- Пример реализации: кабинеты 13, 31, «Кабинет 04» |
|
||||||
|
|
||||||
### 6.4 Брендирование транспорта (трамвай) |
|
||||||
|
|
||||||
**Зоны оклейки:** |
|
||||||
- Борта: Oracal 053M (#0089c3) + 073M (#53514e) |
|
||||||
- Передняя часть: Oracal 066M (#00818c) + 050M (#1b2e5d) |
|
||||||
- Акценты: Oracal 081M (#a8885c) + 080M (#432f1e) |
|
||||||
|
|
||||||
**Все 6 фирменных цветов присутствуют на транспорте.** |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 7. Структура брендбука (страницы) |
|
||||||
|
|
||||||
| URL | Статус | Описание | |
|
||||||
|-----|--------|----------| |
|
||||||
| `/foundation/logo` | ✅ Готова | Логотип, варианты, охранная зона, правила | |
|
||||||
| `/foundation/colors` | ✅ Готова | Палитра (Oracal точные RGB), контраст WCAG, цвета сайта, соответствие | |
|
||||||
| `/foundation/typography` | ✅ Готова | DINPro + Fira Sans, шкала стилей | |
|
||||||
| `/foundation/icons` | 🔜 Скоро | Иконография | |
|
||||||
| `/offline/uniform` | ✅ Готова | Форма сотрудников | |
|
||||||
| `/offline/badges` | ✅ Готова | Бейджи | |
|
||||||
| `/offline/navigation` | ✅ Готова | Внутренняя навигация | |
|
||||||
| `/offline/transport` | ✅ Готова | Брендирование транспорта | |
|
||||||
| `/components/buttons` | ✅ Готова | Кнопки — 4 варианта с реального сайта, размеры, состояния | |
|
||||||
| `/components/forms` | ✅ Готова | Форм-контролы — Input/Textarea/Select/Checkbox/Radio/Toggle | |
|
||||||
| `/components/cards` | ✅ Sprint 4 | Карточки — врач, услуга, новость, отзыв, цена + бейджи/теги/алерты | |
|
||||||
| `/components/*` | 🔜 Sprint 4–5 | Модалки, таблицы, навигация | |
|
||||||
| `/blocks/hero` | ✅ Sprint 5.5 v1.2 | Hero-баннер: H1 36px #cb9768, заголовок 22px #333, кнопка pill, фон #f9f4e7 | |
|
||||||
| `/blocks/doctors` | ✅ Sprint 5.5 v1.2 | Врачи: H2 36px #000, статистика #60959c без фона, 6 реальных фото | |
|
||||||
| `/blocks/*` | 🔜 Sprint 5 | Отзывы, формы, новости, footer | |
|
||||||
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 8. Правила применения цветов |
|
||||||
|
|
||||||
### Разрешено |
|
||||||
- Использовать только цвета из фирменной палитры (раздел 2.1) |
|
||||||
- Для digital: адаптировать к цветам сайта (раздел 2.2) |
|
||||||
- Текст на цветном фоне — минимум WCAG AA (4.5:1) |
|
||||||
- Белый текст на тёмных фонах: 073M (#53514e), 066M (#00818c), 050M (#1b2e5d), 080M (#432f1e) |
|
||||||
- Тёмный текст (#111827) на светлых: 053M (#0089c3), 081M (#a8885c) |
|
||||||
- Коричневый логотип (080M) на бежевом фоне (081M) — только крупный текст (3.8:1) |
|
||||||
|
|
||||||
### Запрещено |
|
||||||
- Использовать произвольные цвета вне палитры |
|
||||||
- Осветлять, затемнять или менять оттенок фирменных цветов |
|
||||||
- Текст с контрастом ниже 3:1 (даже для крупного текста) |
|
||||||
- Размещать тёплые и холодные акценты рядом без разделителя |
|
||||||
|
|
||||||
### Иерархия цветов |
|
||||||
|
|
||||||
``` |
|
||||||
Основной бирюзовый (053M / #0089c3) ← главный идентификатор бренда |
|
||||||
└── Средний бирюзовый (066M / #00818c) ← вторичный акцент |
|
||||||
└── Тёмный серо-голубой (073M / #53514e) ← фоны, хедер |
|
||||||
|
|
||||||
Тёмно-синий (050M / #1b2e5d) ← авторитет, полиграфия |
|
||||||
Бежевый (081M / #a8885c) ← тепло, форма |
|
||||||
└── Тёмно-коричневый (080M / #432f1e) ← текст на бежевом |
|
||||||
``` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 9. Правила применения типографики |
|
||||||
|
|
||||||
### Веб (Fira Sans) |
|
||||||
- H1 только один на странице |
|
||||||
- Заголовки не пропускать по уровням (h1→h2→h3) |
|
||||||
- Основной текст: 16px / 400 / 1.625 |
|
||||||
- Минимальный размер текста на экране: 12px |
|
||||||
- Кириллица: только Fira Sans, не DINPro |
|
||||||
|
|
||||||
### Оффлайн (DINPro) |
|
||||||
- Все физические носители: DINPro |
|
||||||
- Логотип: DINPro Bold |
|
||||||
- Таблички: DINPro Medium / Bold |
|
||||||
- Не использовать light (300) для текста < 14pt |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 9a. Базовые компоненты (Sprint 3) |
|
||||||
|
|
||||||
### Кнопки (Button) · `/components/buttons` |
|
||||||
|
|
||||||
Варианты скопированы с реального сайта perm.oclinica.ru. |
|
||||||
CSS-классы в `globals.css`. Компонент: `@/components/ui/Button` (React, "use client"). |
|
||||||
|
|
||||||
| Вариант | CSS класс | Фон | Текст | Граница | Border-radius | Применение | |
|
||||||
|---------|------------------|----------|---------|-----------|---------------|------------| |
|
||||||
| primary | .bb-btn-primary | #FFA39C | #fff | #FF847B | 7px, bold, shadow | Форма записи «Запишите меня!» | |
|
||||||
| outline | .bb-btn-outline | #fff | #BF9975 | #BF9975 | 7px | «Записаться на приём» | |
|
||||||
| teal | .bb-btn-teal | #60959C | #fff | прозрачная| 7px | «Позвонить» | |
|
||||||
| pill | .bb-btn-pill | #E9E4D4 | #333 | #D5CFBD | 25px | «Заказать звонок» | |
|
||||||
|
|
||||||
| Размер | CSS класс | padding | font-size | |
|
||||||
|--------|------------|--------------|-----------| |
|
||||||
| sm | .bb-btn-sm | 4px 11px | 13px | |
|
||||||
| md | .bb-btn-md | 8px 16px | 14px | |
|
||||||
| lg | .bb-btn-lg | 10px 24px | 18px | |
|
||||||
|
|
||||||
**Состояния:** default · hover (brightness 0.9) · active (brightness 0.82) · loading (spinner) · disabled (opacity 0.5) |
|
||||||
|
|
||||||
### Форм-контролы (Forms) · `/components/forms` |
|
||||||
|
|
||||||
Размеры соответствуют entityform-блокам на реальном сайте. |
|
||||||
|
|
||||||
| Элемент | CSS класс | Тег HTML | Высота | Описание | |
|
||||||
|----------|------------------|----------------------------|---------|----------| |
|
||||||
| Input | .bb-input | `<input>` | 50px | text, email, password · border 1px #ccc · radius 4px | |
|
||||||
| Textarea | .bb-textarea | `<textarea>` | ≥100px | многострочный, resize:vertical | |
|
||||||
| Select | .bb-select | `<select>` | 50px | с кастомной SVG-стрелкой | |
|
||||||
| Checkbox | .bb-checkbox | `<input type="checkbox">` | 16×16px | accent-color: #53514e | |
|
||||||
| Radio | .bb-radio | `<input type="radio">` | 16×16px | accent-color: #53514e | |
|
||||||
| Toggle | .bb-toggle-track | React-компонент `<Toggle>` | 24px | 44×24px track + 20px thumb | |
|
||||||
|
|
||||||
**Состояния полей:** default (border 1px solid #ccc) · focus (border #0089c3 + box-shadow rgba(0,137,195,0.2)) · error (.bb-error, border #dc2626) · disabled (opacity 0.5) |
|
||||||
|
|
||||||
**Контекст на сайте:** фон формы #b8e6ed, ширина полей 302px, entityform-блоки. |
|
||||||
|
|
||||||
**Toggle:** выкл → track #d1d5db · вкл → track #53514e · thumb: белый круг 20×20px. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 9b. Карточки, бейджи и алерты (Sprint 4) |
|
||||||
|
|
||||||
### Карточки · `/components/cards` |
|
||||||
|
|
||||||
| Карточка | Размеры ключевые | Источник на сайте | Фон / hover | |
|
||||||
|-------------|-------------------------|------------------------------------------------|--------------------------| |
|
||||||
| DoctorCard | фото 110×160px | `.doctor .image` + `.doctor .item` | #fff / — | |
|
||||||
| NewsCard | preview h=144px | `#block-views-last-news-block-1 .views-column` | #fff / **#eef4d1** + shadow | |
|
||||||
| ReviewCard | 4-строчный clamp | `.node-reviews` | **#eef4d1** / — | |
|
||||||
| PriceCard | flex row: name + price | `.field-name-field-price-priem` | #fff / highlighted: #f0f9ff | |
|
||||||
| ServiceCard | иконка 48×48px | — (нет прямого аналога) | #fff / shadow | |
|
||||||
|
|
||||||
**DoctorCard:** фото 110×160px (placeholder фон `#dff0fa`), кнопка `.bb-btn-outline .bb-btn-sm` |
|
||||||
**NewsCard hover CSS:** `background: #eef4d1; box-shadow: 0 0 16px 0 #9e9e9a;` — взят с реального сайта |
|
||||||
**ReviewCard:** звёзды SVG, заливка `#f59e0b`, `WebkitLineClamp: 4` |
|
||||||
|
|
||||||
### Бейджи, теги, алерты |
|
||||||
|
|
||||||
| Элемент | Варианты | Применение | |
|
||||||
|---------|----------|------------| |
|
||||||
| Badge | primary (#0089c3) / success (#059669) / warning (#d97706) / danger (#dc2626) / neutral (#6b7280) | Статус врача, категория, акция | |
|
||||||
| Tag | default (border) / active (brand bg) | Фильтры, категории услуг | |
|
||||||
| Alert | info (#dff0fa/#075985) / success (#d1fae5/#065f46) / warning (#fef3c7/#92400e) / error (#fee2e2/#991b1b) | Системные сообщения | |
|
||||||
|
|
||||||
**CSS класс hover:** `.bb-news-card:hover` в globals.css |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 10. Технический стек проекта |
|
||||||
|
|
||||||
| Слой | Технология | Версия | |
|
||||||
|------|-----------|--------| |
|
||||||
| Фронтенд | Next.js App Router | 16.x | |
|
||||||
| Стилизация | Tailwind CSS | 4.x | |
|
||||||
| Язык | TypeScript | 5.x | |
|
||||||
| Шрифты | next/font/google | — | |
|
||||||
| Бэкенд | NestJS | 11.x | |
|
||||||
| БД | PostgreSQL + Prisma | 16.x / 7.x | |
|
||||||
| Деплой | Vercel Hobby | — | |
|
||||||
| Пакетный менеджер | pnpm | 10.x | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 11. История изменений контекста |
|
||||||
|
|
||||||
| Версия | Дата | Что добавлено | |
|
||||||
|--------|------|---------------| |
|
||||||
| 1.0 | 2026-03-22 | Sprint 1: логотип, базовые цвета | |
|
||||||
| 2.0 | 2026-03-22 | Sprint 2: типографика, оффлайн носители, цвета сайта (8 цветов) | |
|
||||||
| 2.1 | 2026-03-22 | Sprint 2 доп.: +3 цвета сайта (коралловый, светло-жёлтый, светло-зелёный) | |
|
||||||
| 3.0 | 2026-03-22 | Sprint 3: кнопки (Button), форм-контролы (Input/Textarea/Select/Checkbox/Radio/Toggle), LLM-блок на логотипе | |
|
||||||
| 4.0 | 2026-03-22 | Sprint 4 start: исправлены цвета Oracal (точные RGB из каталога), кнопки/формы по реальному сайту | |
|
||||||
| 4.1 | 2026-03-22 | Sprint 4 done: карточки (DoctorCard/NewsCard/ReviewCard/PriceCard/ServiceCard), бейджи/теги/алерты | |
|
||||||
| 4.2 | 2026-03-23 | Sprint 5: блоки Hero v1.1, Doctors v1.1 | |
|
||||||
| 4.3 | 2026-03-24 | Sprint 5.5: исправлены CSS-стили блоков по реальному сайту (H1 #cb9768 36px, H2 #000 36px, фоны форм #d4f6f8, фон новостей #f2fee6, CTA-кнопка pill) | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 12. Что обновлять в этом файле |
|
||||||
|
|
||||||
При каждом спринте добавляй: |
|
||||||
- Новые компоненты и их спецификации (цвета, размеры, состояния) |
|
||||||
- Новые правила применения, выявленные при разработке |
|
||||||
- Изменения в палитре или типографике |
|
||||||
- Новые паттерны и примеры кода |
|
||||||
|
|
||||||
**Соответствующий раздел в TZ.md:** ФТ-03-LLM |
|
||||||
**Соответствующий раздел в SPRINTS.md:** задача «LLM-блок» в каждом спринте |
|
||||||