Compare commits
30 Commits
2352e99093
...
60619114a5
| Author | SHA1 | Date |
|---|---|---|
|
|
60619114a5 | 4 weeks ago |
|
|
d4d86e6343 | 4 weeks ago |
|
|
5b1ace962a | 4 weeks ago |
|
|
52f04f9f5d | 4 weeks ago |
|
|
754056642d | 4 weeks ago |
|
|
6ce176f970 | 4 weeks ago |
|
|
6c5b571884 | 4 weeks ago |
|
|
2b66fb9cc1 | 4 weeks ago |
|
|
3811c579fd | 4 weeks ago |
|
|
2b9319e70d | 4 weeks ago |
|
|
d6610a288b | 4 weeks ago |
|
|
77c9733144 | 4 weeks ago |
|
|
c1731615ab | 4 weeks ago |
|
|
0855892643 | 4 weeks ago |
|
|
0198947c4e | 4 weeks ago |
|
|
6648f16bcb | 4 weeks ago |
|
|
761347ed85 | 4 weeks ago |
|
|
52acdc98e2 | 4 weeks ago |
|
|
0570b50d9f | 4 weeks ago |
|
|
789f436be1 | 4 weeks ago |
|
|
5298f8c1cd | 4 weeks ago |
|
|
ff3f6acc03 | 4 weeks ago |
|
|
39ecd72fde | 4 weeks ago |
|
|
a715503ca5 | 4 weeks ago |
|
|
657cc26d3e | 4 weeks ago |
|
|
f03599abcf | 4 weeks ago |
|
|
02f561fcd3 | 4 weeks ago |
|
|
1cdf0e7e95 | 4 weeks ago |
|
|
03cceea13d | 4 weeks ago |
|
|
5105310c92 | 4 weeks ago |
@ -0,0 +1,8 @@ |
|||||||
|
# База данных |
||||||
|
DATABASE_URL="postgresql://brandbook:brandbook@localhost:5433/brandbook" |
||||||
|
|
||||||
|
# API (NestJS) |
||||||
|
API_PORT=3001 |
||||||
|
|
||||||
|
# Web (Next.js) |
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3001 |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
node_modules |
||||||
|
# Keep environment variables out of version control |
||||||
|
.env |
||||||
|
|
||||||
|
/generated/prisma |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"singleQuote": true, |
||||||
|
"trailingComma": "all" |
||||||
|
} |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
<p align="center"> |
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a> |
||||||
|
</p> |
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 |
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest |
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p> |
||||||
|
<p align="center"> |
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a> |
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a> |
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a> |
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a> |
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a> |
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a> |
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a> |
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a> |
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a> |
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a> |
||||||
|
</p> |
||||||
|
<!--[](https://opencollective.com/nest#backer) |
||||||
|
[](https://opencollective.com/nest#sponsor)--> |
||||||
|
|
||||||
|
## Description |
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. |
||||||
|
|
||||||
|
## Project setup |
||||||
|
|
||||||
|
```bash |
||||||
|
$ pnpm install |
||||||
|
``` |
||||||
|
|
||||||
|
## Compile and run the project |
||||||
|
|
||||||
|
```bash |
||||||
|
# development |
||||||
|
$ pnpm run start |
||||||
|
|
||||||
|
# watch mode |
||||||
|
$ pnpm run start:dev |
||||||
|
|
||||||
|
# production mode |
||||||
|
$ pnpm run start:prod |
||||||
|
``` |
||||||
|
|
||||||
|
## Run tests |
||||||
|
|
||||||
|
```bash |
||||||
|
# unit tests |
||||||
|
$ pnpm run test |
||||||
|
|
||||||
|
# e2e tests |
||||||
|
$ pnpm run test:e2e |
||||||
|
|
||||||
|
# test coverage |
||||||
|
$ pnpm run test:cov |
||||||
|
``` |
||||||
|
|
||||||
|
## Deployment |
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. |
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: |
||||||
|
|
||||||
|
```bash |
||||||
|
$ pnpm install -g @nestjs/mau |
||||||
|
$ mau deploy |
||||||
|
``` |
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. |
||||||
|
|
||||||
|
## Resources |
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS: |
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. |
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). |
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). |
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. |
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). |
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). |
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). |
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). |
||||||
|
|
||||||
|
## Support |
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). |
||||||
|
|
||||||
|
## Stay in touch |
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) |
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/) |
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework) |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js'; |
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; |
||||||
|
import globals from 'globals'; |
||||||
|
import tseslint from 'typescript-eslint'; |
||||||
|
|
||||||
|
export default tseslint.config( |
||||||
|
{ |
||||||
|
ignores: ['eslint.config.mjs'], |
||||||
|
}, |
||||||
|
eslint.configs.recommended, |
||||||
|
...tseslint.configs.recommendedTypeChecked, |
||||||
|
eslintPluginPrettierRecommended, |
||||||
|
{ |
||||||
|
languageOptions: { |
||||||
|
globals: { |
||||||
|
...globals.node, |
||||||
|
...globals.jest, |
||||||
|
}, |
||||||
|
sourceType: 'commonjs', |
||||||
|
parserOptions: { |
||||||
|
projectService: true, |
||||||
|
tsconfigRootDir: import.meta.dirname, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
rules: { |
||||||
|
'@typescript-eslint/no-explicit-any': 'off', |
||||||
|
'@typescript-eslint/no-floating-promises': 'warn', |
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn', |
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }], |
||||||
|
}, |
||||||
|
}, |
||||||
|
); |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"$schema": "https://json.schemastore.org/nest-cli", |
||||||
|
"collection": "@nestjs/schematics", |
||||||
|
"sourceRoot": "src", |
||||||
|
"compilerOptions": { |
||||||
|
"deleteOutDir": true |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,75 @@ |
|||||||
|
{ |
||||||
|
"name": "api", |
||||||
|
"version": "0.0.1", |
||||||
|
"description": "", |
||||||
|
"author": "", |
||||||
|
"private": true, |
||||||
|
"license": "UNLICENSED", |
||||||
|
"scripts": { |
||||||
|
"build": "nest build", |
||||||
|
"dev": "nest start --watch", |
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", |
||||||
|
"start": "nest start", |
||||||
|
"start:dev": "nest start --watch", |
||||||
|
"start:debug": "nest start --debug --watch", |
||||||
|
"start:prod": "node dist/main", |
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", |
||||||
|
"test": "jest", |
||||||
|
"test:watch": "jest --watch", |
||||||
|
"test:cov": "jest --coverage", |
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", |
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@nestjs/common": "^11.0.1", |
||||||
|
"@nestjs/core": "^11.0.1", |
||||||
|
"@nestjs/platform-express": "^11.0.1", |
||||||
|
"@prisma/client": "^7.5.0", |
||||||
|
"dotenv": "^17.3.1", |
||||||
|
"reflect-metadata": "^0.2.2", |
||||||
|
"rxjs": "^7.8.1" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@eslint/eslintrc": "^3.2.0", |
||||||
|
"@eslint/js": "^9.18.0", |
||||||
|
"@nestjs/cli": "^11.0.0", |
||||||
|
"@nestjs/schematics": "^11.0.0", |
||||||
|
"@nestjs/testing": "^11.0.1", |
||||||
|
"@types/express": "^5.0.0", |
||||||
|
"@types/jest": "^30.0.0", |
||||||
|
"@types/node": "^22.10.7", |
||||||
|
"@types/supertest": "^6.0.2", |
||||||
|
"eslint": "^9.18.0", |
||||||
|
"eslint-config-prettier": "^10.0.1", |
||||||
|
"eslint-plugin-prettier": "^5.2.2", |
||||||
|
"globals": "^16.0.0", |
||||||
|
"jest": "^30.0.0", |
||||||
|
"prettier": "^3.4.2", |
||||||
|
"prisma": "^7.5.0", |
||||||
|
"source-map-support": "^0.5.21", |
||||||
|
"supertest": "^7.0.0", |
||||||
|
"ts-jest": "^29.2.5", |
||||||
|
"ts-loader": "^9.5.2", |
||||||
|
"ts-node": "^10.9.2", |
||||||
|
"tsconfig-paths": "^4.2.0", |
||||||
|
"typescript": "^5.7.3", |
||||||
|
"typescript-eslint": "^8.20.0" |
||||||
|
}, |
||||||
|
"jest": { |
||||||
|
"moduleFileExtensions": [ |
||||||
|
"js", |
||||||
|
"json", |
||||||
|
"ts" |
||||||
|
], |
||||||
|
"rootDir": "src", |
||||||
|
"testRegex": ".*\\.spec\\.ts$", |
||||||
|
"transform": { |
||||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||||
|
}, |
||||||
|
"collectCoverageFrom": [ |
||||||
|
"**/*.(t|j)s" |
||||||
|
], |
||||||
|
"coverageDirectory": "../coverage", |
||||||
|
"testEnvironment": "node" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
|
// npm install --save-dev prisma dotenv
|
||||||
|
import "dotenv/config"; |
||||||
|
import { defineConfig } from "prisma/config"; |
||||||
|
|
||||||
|
export default defineConfig({ |
||||||
|
schema: "prisma/schema.prisma", |
||||||
|
migrations: { |
||||||
|
path: "prisma/migrations", |
||||||
|
}, |
||||||
|
datasource: { |
||||||
|
url: process.env["DATABASE_URL"], |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
generator client { |
||||||
|
provider = "prisma-client" |
||||||
|
output = "../generated/prisma" |
||||||
|
} |
||||||
|
|
||||||
|
datasource db { |
||||||
|
provider = "postgresql" |
||||||
|
} |
||||||
|
|
||||||
|
enum Role { |
||||||
|
viewer |
||||||
|
editor |
||||||
|
} |
||||||
|
|
||||||
|
enum ComponentStatus { |
||||||
|
draft |
||||||
|
review |
||||||
|
approved |
||||||
|
} |
||||||
|
|
||||||
|
model User { |
||||||
|
id String @id @default(uuid()) |
||||||
|
email String @unique |
||||||
|
name String |
||||||
|
passwordHash String |
||||||
|
role Role @default(viewer) |
||||||
|
createdAt DateTime @default(now()) |
||||||
|
|
||||||
|
components ExperimentalComponent[] |
||||||
|
} |
||||||
|
|
||||||
|
model ExperimentalComponent { |
||||||
|
id String @id @default(uuid()) |
||||||
|
name String |
||||||
|
baseComponent String |
||||||
|
attributes Json |
||||||
|
status ComponentStatus @default(draft) |
||||||
|
author User @relation(fields: [authorId], references: [id]) |
||||||
|
authorId String |
||||||
|
createdAt DateTime @default(now()) |
||||||
|
updatedAt DateTime @updatedAt |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { AppController } from './app.controller'; |
||||||
|
import { AppService } from './app.service'; |
||||||
|
|
||||||
|
describe('AppController', () => { |
||||||
|
let appController: AppController; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const app: TestingModule = await Test.createTestingModule({ |
||||||
|
controllers: [AppController], |
||||||
|
providers: [AppService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('root', () => { |
||||||
|
it('should return "Hello World!"', () => { |
||||||
|
expect(appController.getHello()).toBe('Hello World!'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
import { Controller, Get } from '@nestjs/common'; |
||||||
|
import { AppService } from './app.service'; |
||||||
|
|
||||||
|
@Controller() |
||||||
|
export class AppController { |
||||||
|
constructor(private readonly appService: AppService) {} |
||||||
|
|
||||||
|
@Get() |
||||||
|
getHello(): string { |
||||||
|
return this.appService.getHello(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
import { Module } from '@nestjs/common'; |
||||||
|
import { AppController } from './app.controller'; |
||||||
|
import { AppService } from './app.service'; |
||||||
|
|
||||||
|
@Module({ |
||||||
|
imports: [], |
||||||
|
controllers: [AppController], |
||||||
|
providers: [AppService], |
||||||
|
}) |
||||||
|
export class AppModule {} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import { Injectable } from '@nestjs/common'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class AppService { |
||||||
|
getHello(): string { |
||||||
|
return 'Hello World!'; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import { NestFactory } from '@nestjs/core'; |
||||||
|
import { AppModule } from './app.module'; |
||||||
|
|
||||||
|
async function bootstrap() { |
||||||
|
const app = await NestFactory.create(AppModule); |
||||||
|
await app.listen(process.env.PORT ?? 3000); |
||||||
|
} |
||||||
|
bootstrap(); |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { INestApplication } from '@nestjs/common'; |
||||||
|
import request from 'supertest'; |
||||||
|
import { App } from 'supertest/types'; |
||||||
|
import { AppModule } from './../src/app.module'; |
||||||
|
|
||||||
|
describe('AppController (e2e)', () => { |
||||||
|
let app: INestApplication<App>; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({ |
||||||
|
imports: [AppModule], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication(); |
||||||
|
await app.init(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('/ (GET)', () => { |
||||||
|
return request(app.getHttpServer()) |
||||||
|
.get('/') |
||||||
|
.expect(200) |
||||||
|
.expect('Hello World!'); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
{ |
||||||
|
"moduleFileExtensions": ["js", "json", "ts"], |
||||||
|
"rootDir": ".", |
||||||
|
"testEnvironment": "node", |
||||||
|
"testRegex": ".e2e-spec.ts$", |
||||||
|
"transform": { |
||||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"module": "nodenext", |
||||||
|
"moduleResolution": "nodenext", |
||||||
|
"resolvePackageJsonExports": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"isolatedModules": true, |
||||||
|
"declaration": true, |
||||||
|
"removeComments": true, |
||||||
|
"emitDecoratorMetadata": true, |
||||||
|
"experimentalDecorators": true, |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"target": "ES2023", |
||||||
|
"sourceMap": true, |
||||||
|
"outDir": "./dist", |
||||||
|
"baseUrl": "./", |
||||||
|
"incremental": true, |
||||||
|
"skipLibCheck": true, |
||||||
|
"strictNullChecks": true, |
||||||
|
"forceConsistentCasingInFileNames": true, |
||||||
|
"noImplicitAny": false, |
||||||
|
"strictBindCallApply": false, |
||||||
|
"noFallthroughCasesInSwitch": false |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
||||||
|
|
||||||
|
# dependencies |
||||||
|
/node_modules |
||||||
|
/.pnp |
||||||
|
.pnp.* |
||||||
|
.yarn/* |
||||||
|
!.yarn/patches |
||||||
|
!.yarn/plugins |
||||||
|
!.yarn/releases |
||||||
|
!.yarn/versions |
||||||
|
|
||||||
|
# testing |
||||||
|
/coverage |
||||||
|
|
||||||
|
# next.js |
||||||
|
/.next/ |
||||||
|
/out/ |
||||||
|
|
||||||
|
# production |
||||||
|
/build |
||||||
|
|
||||||
|
# misc |
||||||
|
.DS_Store |
||||||
|
*.pem |
||||||
|
|
||||||
|
# debug |
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
||||||
|
.pnpm-debug.log* |
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed) |
||||||
|
.env* |
||||||
|
|
||||||
|
# vercel |
||||||
|
.vercel |
||||||
|
|
||||||
|
# typescript |
||||||
|
*.tsbuildinfo |
||||||
|
next-env.d.ts |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
<!-- BEGIN:nextjs-agent-rules --> |
||||||
|
# This is NOT the Next.js you know |
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. |
||||||
|
<!-- END:nextjs-agent-rules --> |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). |
||||||
|
|
||||||
|
## Getting Started |
||||||
|
|
||||||
|
First, run the development server: |
||||||
|
|
||||||
|
```bash |
||||||
|
npm run dev |
||||||
|
# or |
||||||
|
yarn dev |
||||||
|
# or |
||||||
|
pnpm dev |
||||||
|
# or |
||||||
|
bun dev |
||||||
|
``` |
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. |
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. |
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. |
||||||
|
|
||||||
|
## Learn More |
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources: |
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. |
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. |
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! |
||||||
|
|
||||||
|
## Deploy on Vercel |
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. |
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. |
||||||
@ -0,0 +1,224 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
const CEO_QUESTIONS = [ |
||||||
|
"У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?", |
||||||
|
"Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?", |
||||||
|
"Заболел ребёнок?", |
||||||
|
"Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых с удобным режимом работы с 9:00 до 21:00 по будням?", |
||||||
|
"Вам назначили проведение эндоскопической операции на ухе, горле или носе?", |
||||||
|
]; |
||||||
|
|
||||||
|
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> |
||||||
|
<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 p-8" |
||||||
|
style={{ |
||||||
|
background: "#fff", |
||||||
|
border: "1px solid var(--bb-border)", |
||||||
|
fontFamily: "var(--font-web)", |
||||||
|
fontSize: 14, |
||||||
|
lineHeight: 1.75, |
||||||
|
color: "#374151", |
||||||
|
}} |
||||||
|
> |
||||||
|
<p> |
||||||
|
Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей |
||||||
|
с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах:{" "} |
||||||
|
<a href="#" style={{ color: "#0089c3" }}>ул. Цитная, 9</a>, <a href="#" style={{ color: "#0089c3" }}>ул. Г. Звезда, 31а</a>.{" "} |
||||||
|
Это <a href="#" style={{ color: "#0089c3" }}>Клиника лечения кашля и аллергии</a>. |
||||||
|
</p> |
||||||
|
|
||||||
|
<div className="mt-4 space-y-2"> |
||||||
|
{CEO_QUESTIONS.map((q) => ( |
||||||
|
<p key={q}>— {q}</p> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
|
||||||
|
<p className="mt-4"> |
||||||
|
Обращайтесь в ЛОР центр ухо, горло, нос в Перми, наши врачи оториноларингологи обязательно Вам помогут! |
||||||
|
</p> |
||||||
|
<p className="mt-2"> |
||||||
|
Клиника ЛОР болезней ухо, горло, нос – это наиболее современный центр оториноларингологии в Перми, |
||||||
|
благодаря эндоскопическому оборудованию, высокому профессионализму оториноларингологов. |
||||||
|
</p> |
||||||
|
<p className="mt-4 font-medium">Будьте здоровы!</p> |
||||||
|
</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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,236 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
const LLM_FORMS_TEXT = ` |
||||||
|
БЛОК: Формы записи и обратной связи |
||||||
|
Источник: perm.oclinica.ru/lor — два блока форм на одной странице |
||||||
|
Версия: v1.0 |
||||||
|
|
||||||
|
ФОРМА 1: «Будьте здоровы!» (форма записи на приём) |
||||||
|
Позиция: после блока отзывов |
||||||
|
Заголовок H2: «Будьте здоровы!» |
||||||
|
Подзаголовок: «Запишитесь на приём к врачу!» |
||||||
|
Фон секции: #b8e6ed (светло-бирюзовый) |
||||||
|
Поля формы: |
||||||
|
1. Текстовый input: placeholder «Введите ваше имя» (height 50px) |
||||||
|
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px) |
||||||
|
3. Select: «Выберите ЛОР врача» (height 50px) |
||||||
|
4. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных» |
||||||
|
Кнопка: «Запишите меня!» — стиль bb-btn-primary (#FFA39C) |
||||||
|
Ширина формы: ~400px центрирована или в колонку |
||||||
|
|
||||||
|
ФОРМА 2: «Узнайте стоимость операции» (консультация) |
||||||
|
Позиция: после блока новостей |
||||||
|
Заголовок H2: «Узнайте стоимость операции» |
||||||
|
Подзаголовок: «Проконсультируйтесь с ассистентом хирурга» |
||||||
|
Фон секции: #ffffff |
||||||
|
Поля формы: |
||||||
|
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) |
||||||
|
— Форма 1 на бирюзовом фоне (#b8e6ed), форма 2 на белом |
||||||
|
|
||||||
|
ПРАВИЛА: |
||||||
|
✓ Чекбокс согласия обязателен в каждой форме |
||||||
|
✓ Кнопки отправки: bb-btn-primary (#FFA39C) |
||||||
|
✓ Форма 1 на фоне #b8e6ed |
||||||
|
✕ Не менять порядок полей |
||||||
|
✕ Не убирать чекбокс согласия |
||||||
|
`.trim();
|
||||||
|
|
||||||
|
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> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Форма 1: Будьте здоровы */} |
||||||
|
<section className="space-y-3"> |
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
||||||
|
Форма 1 — «Будьте здоровы!» |
||||||
|
</h2> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Фон секции: #b8e6ed. Запись на приём к врачу. |
||||||
|
</p> |
||||||
|
<div |
||||||
|
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6" |
||||||
|
style={{ background: "#b8e6ed" }} |
||||||
|
> |
||||||
|
<div className="text-center"> |
||||||
|
<h2 className="text-2xl font-bold mb-1" style={{ color: "#111827" }}> |
||||||
|
Будьте здоровы! |
||||||
|
</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> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* Форма 2: Узнайте стоимость */} |
||||||
|
<section className="space-y-3"> |
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
||||||
|
Форма 2 — «Узнайте стоимость операции» |
||||||
|
</h2> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Фон секции: #ffffff. Запрос консультации по стоимости. |
||||||
|
</p> |
||||||
|
<div |
||||||
|
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6" |
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }} |
||||||
|
> |
||||||
|
<div className="text-center"> |
||||||
|
<h2 className="text-2xl font-bold mb-1" style={{ color: "#111827" }}> |
||||||
|
Узнайте стоимость операции |
||||||
|
</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> |
||||||
|
</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> |
||||||
|
{[ |
||||||
|
["Заголовок", "«Будьте здоровы!»", "«Узнайте стоимость операции»"], |
||||||
|
["Подзаголовок", "«Запишитесь на приём к врачу!»", "«Проконсультируйтесь с ассистентом хирурга»"], |
||||||
|
["Фон секции", "#b8e6ed", "#ffffff"], |
||||||
|
["Поля", "Имя + Телефон + 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.0" 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: "Форма 1: фон #b8e6ed, форма 2: фон #fff" }, |
||||||
|
{ 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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,257 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
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> |
||||||
|
<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> |
||||||
|
<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> |
||||||
|
</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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,238 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
const STATS = [ |
||||||
|
{ num: "27", label: "ЛОР врачей работает в клинике", prefix: "Ежедневно" }, |
||||||
|
{ num: "6", label: "кандидатов медицинских наук", prefix: "В том числе" }, |
||||||
|
{ num: "12 000+", label: "успешно проведённых операций", prefix: "Свыше" }, |
||||||
|
]; |
||||||
|
|
||||||
|
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", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const LLM_DOCTORS_TEXT = ` |
||||||
|
БЛОК: Наши врачи |
||||||
|
Источник: perm.oclinica.ru/lor — блок под CEO-текстом |
||||||
|
Версия: v1.0 |
||||||
|
|
||||||
|
СТРУКТУРА БЛОКА: |
||||||
|
1. ЗАГОЛОВОК H2: «Приём ведут опытные ЛОР врачи» |
||||||
|
Подзаголовок: описание принципа работы врачей клиники |
||||||
|
|
||||||
|
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: #111827, ~30px (text-3xl) |
||||||
|
Статистика текст: #60959c (серо-бирюзовый) |
||||||
|
Статистика черта: border-bottom 3px solid #60959c |
||||||
|
Имя врача: #60959c |
||||||
|
Специализация: #374151 |
||||||
|
|
||||||
|
ПРАВИЛА: |
||||||
|
✓ Заголовок H2 + описание обязательны |
||||||
|
✓ 3 stat-блока в ряд |
||||||
|
✓ Сетка 3 колонки, 2 ряда (6 карточек) |
||||||
|
✕ Не отображать более 6 врачей в основном блоке |
||||||
|
✕ Не убирать статистику |
||||||
|
`.trim();
|
||||||
|
|
||||||
|
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> |
||||||
|
<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 space-y-8" |
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }} |
||||||
|
> |
||||||
|
{/* Заголовок */} |
||||||
|
<div> |
||||||
|
<h2 className="text-3xl font-bold mb-3" style={{ color: "#111827" }}> |
||||||
|
Приём ведут опытные ЛОР врачи |
||||||
|
</h2> |
||||||
|
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}> |
||||||
|
Фундаментальная теоретическая подготовка и большой практический опыт в сочетании |
||||||
|
с внимательным индивидуальным подходом являются причиной успеха лечения тысяч наших пациентов |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Статистика — текст тёмно-бирюзовым, без фоновых блоков */} |
||||||
|
<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> |
||||||
|
|
||||||
|
{/* Сетка врачей — плотнее, имена тёмно-бирюзовым */} |
||||||
|
<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> |
||||||
|
</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.1" specText={LLM_DOCTORS_TEXT}> |
||||||
|
<LlmSection title="Структура блока" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Элемент", "Содержимое", "Стиль"]} |
||||||
|
rows={[ |
||||||
|
["H2", "«Приём ведут опытные ЛОР врачи»", "~30px (text-3xl), font-bold, #111827"], |
||||||
|
["Подзаголовок", "Описание принципа работы", "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={[ |
||||||
|
["Фото", "88×120px (в блоке) или 110×160px (на странице врачей), object-fit: cover"], |
||||||
|
["Имя", "12–14px, font-weight 500, #111827"], |
||||||
|
["Специализация", "11–12px, #6b7280"], |
||||||
|
]} |
||||||
|
/> |
||||||
|
<LlmSection title="Правила" /> |
||||||
|
<LlmRules |
||||||
|
rules={[ |
||||||
|
{ ok: true, text: "H2 + описание обязательны" }, |
||||||
|
{ ok: true, text: "3 stat-блока в ряд, фон #dff0fa" }, |
||||||
|
{ ok: true, text: "Сетка 6 карточек (3 колонки)" }, |
||||||
|
{ ok: false, text: "Не показывать более 6 врачей в основном блоке" }, |
||||||
|
{ ok: false, text: "Не убирать статистику" }, |
||||||
|
]} |
||||||
|
/> |
||||||
|
</LlmBlock> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,286 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
const LLM_HERO_TEXT = ` |
||||||
|
БЛОК: Hero-баннер (главный баннер страницы) |
||||||
|
Источник: perm.oclinica.ru/lor — реальный баннер раздела ЛОР |
||||||
|
Версия: v1.1 |
||||||
|
|
||||||
|
ЗАГОЛОВОК СТРАНИЦЫ (H1, над баннером): |
||||||
|
«ЛОР Клиника ухо, горло, нос – медицинский центр лечения ЛОР заболеваний у детей и взрослых» |
||||||
|
Шрифт: Fira Sans, ~26–28px, weight 700, цвет: #53514e (--brand-073m) |
||||||
|
|
||||||
|
СТРУКТУРА БАННЕРА (двухколоночная, единый фон #f9f4e7): |
||||||
|
Левая колонка (~50%): |
||||||
|
— Заголовок: «ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ ЛЕЧЕНИЕ ЛОР ОРГАНОВ» |
||||||
|
Шрифт: Fira Sans, ~18px, weight 700, uppercase, цвет #111827 |
||||||
|
— 3 пункта с галочками (✓ зелёный): |
||||||
|
1. «БЕЗОПАСНО – оперируют хирурги с 15-летним опытом работы» |
||||||
|
2. «БЕЗ ВНЕШНИХ РАЗРЕЗОВ – хирургия сверхмалых размеров» |
||||||
|
3. «БЫСТРО – под наблюдением врача пациент находится 1 сутки» |
||||||
|
Ключевое слово: uppercase bold; описание: обычный текст, ~13px |
||||||
|
— Кнопка «Узнать стоимость операции» — стиль: bb-btn-outline (светлая, без заливки) |
||||||
|
Правая колонка (~50%): |
||||||
|
— Фото врача с пациентом |
||||||
|
— Изображение занимает всю высоту блока |
||||||
|
|
||||||
|
ПОД БАННЕРОМ: |
||||||
|
— Кнопки соцсетей (Facebook, VK, Twitter/X), цвет #6b7280 |
||||||
|
— Счётчик просмотров |
||||||
|
|
||||||
|
ЦВЕТА: |
||||||
|
Фон баннера: #f9f4e7 (светло-кремовый, единый для всего блока) |
||||||
|
Кнопка CTA: outline-стиль (светлая), не коралловая |
||||||
|
Заголовок блока: #111827 |
||||||
|
Пункты: ключевое слово #111827 bold, описание #374151 |
||||||
|
Галочка: цвет бренда (бежевый/золотой ~#bf9975) |
||||||
|
|
||||||
|
ПРАВИЛА: |
||||||
|
✓ Фон баннера всегда #f9f4e7 (светло-кремовый) — единый, без разделения на зоны |
||||||
|
✓ Заголовок блока uppercase, жирный |
||||||
|
✓ Три пункта с галочками обязательны |
||||||
|
✕ Не менять фон баннера на другой цвет |
||||||
|
✕ Не разбивать фон на два разных цвета |
||||||
|
✕ Не убирать три пункта с галочками |
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const HERO_CHECKS = [ |
||||||
|
{ |
||||||
|
key: "БЕЗОПАСНО", |
||||||
|
desc: "оперируют хирурги с 15-летним опытом работы", |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: "БЕЗ ВНЕШНИХ РАЗРЕЗОВ", |
||||||
|
desc: "хирургия смяткими размерами", |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: "БЫСТРО", |
||||||
|
desc: "под наблюдением врача пациент находится 1 сутки", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
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> |
||||||
|
<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> |
||||||
|
|
||||||
|
{/* H1 страницы */} |
||||||
|
<h2 |
||||||
|
className="text-xl font-bold leading-snug" |
||||||
|
style={{ color: "#53514e" }} |
||||||
|
> |
||||||
|
ЛОР Клиника ухо, горло, нос – медицинский центр лечения ЛОР заболеваний у детей и взрослых |
||||||
|
</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="text-base font-bold uppercase leading-snug" |
||||||
|
style={{ color: "#111827" }} |
||||||
|
> |
||||||
|
Эндоскопическое хирургическое лечение ЛОР органов |
||||||
|
</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-outline"> |
||||||
|
Узнать стоимость операции |
||||||
|
</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: "var(--bb-text-muted)" }}> |
||||||
|
Поделиться: |
||||||
|
</span> |
||||||
|
{["VK", "FB", "TW"].map((s) => ( |
||||||
|
<button |
||||||
|
key={s} |
||||||
|
className="text-xs px-2 py-1 rounded" |
||||||
|
style={{ |
||||||
|
background: "var(--bb-sidebar-bg)", |
||||||
|
border: "1px solid var(--bb-border)", |
||||||
|
color: "var(--bb-text-muted)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{s} |
||||||
|
</button> |
||||||
|
))} |
||||||
|
<span className="text-xs ml-2" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
👁 98 573 просмотра |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</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: "#22c55e" }}> |
||||||
|
✓ |
||||||
|
</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. Описание: обычный текст. Галочка: #22c55e. |
||||||
|
</p> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* LLM блок */} |
||||||
|
<LlmBlock path="/blocks/hero" version="v1.1" specText={LLM_HERO_TEXT}> |
||||||
|
<LlmSection title="Структура баннера" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Зона", "Ширина", "Фон", "Содержимое"]} |
||||||
|
rows={[ |
||||||
|
["Весь баннер", "100%", "#f9f4e7", "Единый светло-кремовый фон"], |
||||||
|
["Левый блок", "~50%", "#f9f4e7 (общий)", "Заголовок uppercase + 3 галочки + кнопка outline"], |
||||||
|
["Правый блок", "~50%", "#f9f4e7 (общий)", "Фото врача с пациентом"], |
||||||
|
["Под баннером", "100%", "#fff", "Кнопки соцсетей + счётчик просмотров"], |
||||||
|
]} |
||||||
|
/> |
||||||
|
<LlmSection title="Три пункта баннера" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Ключевое слово", "Описание"]} |
||||||
|
rows={HERO_CHECKS.map((c) => [c.key, c.desc])} |
||||||
|
/> |
||||||
|
<LlmSection title="Цвета" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Элемент", "Цвет", "Токен"]} |
||||||
|
rows={[ |
||||||
|
["Фон баннера (единый)", "#f9f4e7", "Светло-кремовый фон"], |
||||||
|
["Кнопка CTA", "outline-стиль", "bb-btn-outline"], |
||||||
|
["Заголовок блока", "#111827", "—"], |
||||||
|
["Галочка ✓", "#bf9975", "Бежевый (--brand-081m approx.)"], |
||||||
|
]} |
||||||
|
/> |
||||||
|
<LlmSection title="Правила" /> |
||||||
|
<LlmRules |
||||||
|
rules={[ |
||||||
|
{ ok: true, text: "Фон баннера: #f9f4e7 (светло-кремовый) — единый для всего блока" }, |
||||||
|
{ ok: true, text: "Кнопка CTA: bb-btn-outline (светлая)" }, |
||||||
|
{ ok: true, text: "Заголовок: uppercase, bold" }, |
||||||
|
{ ok: true, text: "Три пункта с галочками ✓ (#bf9975)" }, |
||||||
|
{ ok: false, text: "Не менять фон баннера на другой цвет" }, |
||||||
|
{ ok: false, text: "Не разбивать баннер на два разных цвета фона" }, |
||||||
|
{ ok: false, text: "Не убирать три пункта с галочками" }, |
||||||
|
]} |
||||||
|
/> |
||||||
|
</LlmBlock> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,198 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
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.0 |
||||||
|
|
||||||
|
СТРУКТУРА БЛОКА: |
||||||
|
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();
|
||||||
|
|
||||||
|
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> |
||||||
|
<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> |
||||||
|
<div |
||||||
|
className="rounded-xl p-8 space-y-6" |
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }} |
||||||
|
> |
||||||
|
<h2 className="text-xl font-bold" style={{ color: "#111827" }}> |
||||||
|
Новости |
||||||
|
</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> |
||||||
|
</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.0" 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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,264 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Блок «Отзывы». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
const MOCK_REVIEWS = [ |
||||||
|
{ |
||||||
|
text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.", |
||||||
|
author: "Пациент клиники", |
||||||
|
doctor: "Тимофеева Наталья Александровна", |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.", |
||||||
|
author: "Наталья К.", |
||||||
|
doctor: "Макарова Людмила Тимофеевна", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const LLM_REVIEWS_TEXT = ` |
||||||
|
БЛОК: Отзывы о нас |
||||||
|
Источник: perm.oclinica.ru/lor — блок отзывов |
||||||
|
Версия: v1.0 |
||||||
|
|
||||||
|
СТРУКТУРА БЛОКА: |
||||||
|
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();
|
||||||
|
|
||||||
|
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> |
||||||
|
<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> |
||||||
|
<div |
||||||
|
className="rounded-xl p-8 space-y-6" |
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }} |
||||||
|
> |
||||||
|
{/* Заголовок */} |
||||||
|
<div> |
||||||
|
<h2 className="text-xl font-bold mb-2" style={{ color: "#111827" }}> |
||||||
|
Отзывы о нас |
||||||
|
</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 relative" |
||||||
|
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> |
||||||
|
</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.0" specText={LLM_REVIEWS_TEXT}> |
||||||
|
<LlmSection title="Структура блока" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Элемент", "Содержимое", "Стиль"]} |
||||||
|
rows={[ |
||||||
|
["H2", "«Отзывы о нас»", "text-xl, font-bold, #111827"], |
||||||
|
["Подзаголовок", "Описание достижений клиники за 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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,573 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { Button } from "@/components/ui/Button"; |
||||||
|
import { CodeCopy } from "@/components/ui/CodeCopy"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Кнопки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
id, |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
id?: string; |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section id={id} className="mb-12"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
{title} |
||||||
|
</h2> |
||||||
|
{subtitle && ( |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{subtitle} |
||||||
|
</p> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
{children} |
||||||
|
</section> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const 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> |
||||||
|
<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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,629 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { CodeCopy } from "@/components/ui/CodeCopy"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Карточки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
id, title, subtitle, children, |
||||||
|
}: { |
||||||
|
id?: string; title: string; subtitle?: string; children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section id={id} className="mb-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> |
||||||
|
<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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,723 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { Toggle } from "@/components/ui/Toggle"; |
||||||
|
import { CodeCopy } from "@/components/ui/CodeCopy"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Форм-контролы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
id, |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
id?: string; |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section id={id} className="mb-12"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
{title} |
||||||
|
</h2> |
||||||
|
{subtitle && ( |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{subtitle} |
||||||
|
</p> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
{children} |
||||||
|
</section> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function FieldLabel({ text, required }: { text: string; required?: boolean }) { |
||||||
|
return ( |
||||||
|
<label |
||||||
|
className="block text-sm font-medium mb-1.5" |
||||||
|
style={{ color: "var(--bb-text)" }} |
||||||
|
> |
||||||
|
{text} |
||||||
|
{required && <span style={{ color: "#dc2626", marginLeft: 2 }}>*</span>} |
||||||
|
</label> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function FieldHint({ text }: { text: string }) { |
||||||
|
return ( |
||||||
|
<p className="mt-1.5 text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{text} |
||||||
|
</p> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function FieldError({ text }: { text: string }) { |
||||||
|
return ( |
||||||
|
<p className="mt-1.5 text-xs" style={{ color: "#dc2626" }}> |
||||||
|
{text} |
||||||
|
</p> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function StateCard({ |
||||||
|
label, |
||||||
|
hint, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
label: string; |
||||||
|
hint: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<div className="rounded-xl border p-4" style={{ borderColor: "var(--bb-border)" }}> |
||||||
|
<div className="mb-3">{children}</div> |
||||||
|
<p className="text-sm font-medium mb-0.5" style={{ color: "var(--bb-text)" }}> |
||||||
|
{label} |
||||||
|
</p> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{hint} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const LLM_FORMS_TEXT = `ФОРМ-КОНТРОЛЫ — LLM-спецификация
|
||||||
|
Версия: 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> |
||||||
|
<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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,300 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
const NAV_ITEMS = [ |
||||||
|
"Клиника", |
||||||
|
"ЛОР врачи", |
||||||
|
"Заболевания", |
||||||
|
"Вопрос-ответ", |
||||||
|
"ЛОР-операции", |
||||||
|
"Сурдология", |
||||||
|
"Цены", |
||||||
|
"Контакты", |
||||||
|
]; |
||||||
|
|
||||||
|
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> |
||||||
|
<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" |
||||||
|
style={{ border: "1px solid var(--bb-border)", 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 bb-nav-link whitespace-nowrap" |
||||||
|
style={{ |
||||||
|
color: i === 0 ? "#0089c3" : "#53514e", |
||||||
|
borderRight: "1px solid #f3f4f6", |
||||||
|
fontWeight: i === 0 ? 500 : 400, |
||||||
|
textDecoration: "none", |
||||||
|
}} |
||||||
|
> |
||||||
|
{item} |
||||||
|
</a> |
||||||
|
))} |
||||||
|
</nav> |
||||||
|
</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="/components/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> |
||||||
|
); |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,821 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useState, useCallback } from "react"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
import type { Metadata } from "next"; |
||||||
|
|
||||||
|
/* ─── Утилиты конвертации ──────────────────────────────────────────── */ |
||||||
|
function hexToRgb(hex: string): { r: number; g: number; b: number } { |
||||||
|
const m = /^#([0-9a-f]{6})$/i.exec(hex); |
||||||
|
if (!m) return { r: 0, g: 0, b: 0 }; |
||||||
|
return { |
||||||
|
r: parseInt(m[1].slice(0, 2), 16), |
||||||
|
g: parseInt(m[1].slice(2, 4), 16), |
||||||
|
b: parseInt(m[1].slice(4, 6), 16), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } { |
||||||
|
r /= 255; g /= 255; b /= 255; |
||||||
|
const max = Math.max(r, g, b), min = Math.min(r, g, b); |
||||||
|
let h = 0, s = 0; |
||||||
|
const l = (max + min) / 2; |
||||||
|
if (max !== min) { |
||||||
|
const d = max - min; |
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); |
||||||
|
switch (max) { |
||||||
|
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; |
||||||
|
case g: h = ((b - r) / d + 2) / 6; break; |
||||||
|
case b: h = ((r - g) / d + 4) / 6; break; |
||||||
|
} |
||||||
|
} |
||||||
|
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; |
||||||
|
} |
||||||
|
|
||||||
|
function luminance(r: number, g: number, b: number): number { |
||||||
|
const a = [r, g, b].map(v => { |
||||||
|
v /= 255; |
||||||
|
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); |
||||||
|
}); |
||||||
|
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; |
||||||
|
} |
||||||
|
|
||||||
|
function contrastRatio(hex1: string, hex2: string): number { |
||||||
|
const { r: r1, g: g1, b: b1 } = hexToRgb(hex1); |
||||||
|
const { r: r2, g: g2, b: b2 } = hexToRgb(hex2); |
||||||
|
const l1 = luminance(r1, g1, b1); |
||||||
|
const l2 = luminance(r2, g2, b2); |
||||||
|
const lighter = Math.max(l1, l2); |
||||||
|
const darker = Math.min(l1, l2); |
||||||
|
return Math.round(((lighter + 0.05) / (darker + 0.05)) * 10) / 10; |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Данные цветов ────────────────────────────────────────────────── */ |
||||||
|
const BRAND_COLORS = [ |
||||||
|
{ |
||||||
|
oracal: "053M", |
||||||
|
name: "Основной бирюзовый", |
||||||
|
hex: "#0089c3", |
||||||
|
usage: "Акцентный цвет, CTA-кнопки, иконки, активные состояния", |
||||||
|
cssVar: "--brand-053m", |
||||||
|
}, |
||||||
|
{ |
||||||
|
oracal: "073M", |
||||||
|
name: "Тёмный серо-голубой", |
||||||
|
hex: "#53514e", |
||||||
|
usage: "Тёмный фон, хедер, акценты на тёмных поверхностях", |
||||||
|
cssVar: "--brand-073m", |
||||||
|
}, |
||||||
|
{ |
||||||
|
oracal: "066M", |
||||||
|
name: "Средний бирюзовый", |
||||||
|
hex: "#00818c", |
||||||
|
usage: "Вторичные акценты, фоны секций, иллюстрации", |
||||||
|
cssVar: "--brand-066m", |
||||||
|
}, |
||||||
|
{ |
||||||
|
oracal: "050M", |
||||||
|
name: "Тёмно-синий", |
||||||
|
hex: "#1b2e5d", |
||||||
|
usage: "Наружная реклама, полиграфия, заголовки на светлом фоне", |
||||||
|
cssVar: "--brand-050m", |
||||||
|
}, |
||||||
|
{ |
||||||
|
oracal: "081M", |
||||||
|
name: "Бежевый", |
||||||
|
hex: "#a8885c", |
||||||
|
usage: "Форма сотрудников, оффлайн носители, тёплые акценты", |
||||||
|
cssVar: "--brand-081m", |
||||||
|
}, |
||||||
|
{ |
||||||
|
oracal: "080M", |
||||||
|
name: "Тёмно-коричневый", |
||||||
|
hex: "#432f1e", |
||||||
|
usage: "Текст на бежевых поверхностях, логотип на форме", |
||||||
|
cssVar: "--brand-080m", |
||||||
|
}, |
||||||
|
{ |
||||||
|
oracal: "—", |
||||||
|
name: "Белый", |
||||||
|
hex: "#ffffff", |
||||||
|
usage: "Фон, инвертированный текст, логотип на тёмных фонах", |
||||||
|
cssVar: "--brand-white", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
/* ─── Соответствие цветов ──────────────────────────────────────────── */ |
||||||
|
const COLOR_MAPPING = [ |
||||||
|
{ |
||||||
|
brand: { oracal: "053M", name: "Основной бирюзовый", hex: "#0089c3" }, |
||||||
|
web: { name: "Бирюзовый", hex: "#63bac3", count: 4 }, |
||||||
|
note: "Oracal — насыщенный синий; сайт — светлее и голубее", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: { oracal: "073M", name: "Тёмный серо-голубой", hex: "#53514e" }, |
||||||
|
web: { name: "Серо-бирюзовый", hex: "#60959c", count: 7 }, |
||||||
|
note: "Oracal — тёмно-серый; сайт — серо-бирюзовый (значительное расхождение)", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: { oracal: "066M", name: "Средний бирюзовый", hex: "#00818c" }, |
||||||
|
web: { name: "Бирюзовый средний", hex: "#52b4bd", count: 4 }, |
||||||
|
note: "Oracal — тёмный бирюзовый; сайт — светлее", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: { oracal: "081M", name: "Бежевый", hex: "#a8885c" }, |
||||||
|
web: { name: "Бежевый", hex: "#bf9975", count: 12 }, |
||||||
|
note: "Oracal — тёплый охристый; сайт — прохладнее и светлее", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: { oracal: "050M", name: "Тёмно-синий", hex: "#1b2e5d" }, |
||||||
|
web: null, |
||||||
|
note: "Не найден в CSS сайта", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: { oracal: "080M", name: "Тёмно-коричневый", hex: "#432f1e" }, |
||||||
|
web: null, |
||||||
|
note: "Не найден в CSS сайта", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Основной текст", hex: "#464646", count: 3 }, |
||||||
|
note: "Только на сайте — нет Oracal-аналога", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Второстепенный текст", hex: "#949290", count: 4 }, |
||||||
|
note: "Только на сайте — нет Oracal-аналога", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Светло-кремовый фон (Hero)", hex: "#f9f4e7", count: 1 }, |
||||||
|
note: "Только на сайте — фон Hero-баннера страниц разделов. Нет Oracal-аналога", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Кремовый фон", hex: "#e9e4d4", count: 1 }, |
||||||
|
note: "Только на сайте — тёплые фоны кнопок-пилюль, вторичные секции. Нет Oracal-аналога", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Коралловый", hex: "#ffa39c", count: 2 }, |
||||||
|
note: "Только на сайте — CTA-кнопки, нет в оффлайн-палитре", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Светло-жёлтый фон", hex: "#eef4d1", count: 1 }, |
||||||
|
note: "Только на сайте — фон карточек отзывов", |
||||||
|
}, |
||||||
|
{ |
||||||
|
brand: null, |
||||||
|
web: { name: "Светло-зелёный фон", hex: "#f2fee6", count: 1 }, |
||||||
|
note: "Только на сайте — фон секции новостей", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
/* ─── Цвета с сайта ────────────────────────────────────────────────── */ |
||||||
|
// Источник: https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css
|
||||||
|
// Извлечены парсингом CSS: grep + python Counter по property/value, 2026-03-22
|
||||||
|
const WEB_COLORS = [ |
||||||
|
{ name: "Бежевый", hex: "#bf9975", usage: "Основной тёплый акцент, фоны, рамки, текст", count: 12, group: "Акценты" }, |
||||||
|
{ name: "Серо-бирюзовый", hex: "#60959c", usage: "Основной холодный акцент, ссылки", count: 7, group: "Акценты" }, |
||||||
|
{ name: "Бирюзовый", hex: "#63bac3", usage: "Фоны акцентных блоков, иконки", count: 4, group: "Акценты" }, |
||||||
|
{ name: "Бирюзовый средний", hex: "#52b4bd", usage: "Вторичные цветовые акценты", count: 4, group: "Акценты" }, |
||||||
|
{ name: "Основной текст", hex: "#464646", usage: "Цвет основного текста на сайте", count: 3, group: "Текст" }, |
||||||
|
{ name: "Второстепенный текст", hex: "#949290", usage: "Подписи, второстепенный контент", count: 4, group: "Текст" }, |
||||||
|
{ name: "Светло-бирюзовый фон", hex: "#b8e6ed", usage: "Фон формы записи («Будьте здоровы!»)", count: 1, group: "Фоны" }, |
||||||
|
{ name: "Светло-кремовый фон (Hero)", hex: "#f9f4e7", usage: "Фон Hero-баннера страниц разделов", count: 1, group: "Фоны" }, |
||||||
|
{ name: "Кремовый фон", hex: "#e9e4d4", usage: "Тёплые фоны кнопок-пилюль, вторичные секции", count: 1, group: "Фоны" }, |
||||||
|
{ name: "Коралловый", hex: "#ffa39c", usage: "CTA-кнопки («Запишите меня!»), акцентные призывы к действию", count: 2, group: "Акценты" }, |
||||||
|
{ name: "Светло-жёлтый фон", hex: "#eef4d1", usage: "Фон карточек отзывов (секция «Отзывы о нас»)", count: 1, group: "Фоны" }, |
||||||
|
{ name: "Светло-зелёный фон", hex: "#f2fee6", usage: "Фон секции новостей", count: 1, group: "Фоны" }, |
||||||
|
]; |
||||||
|
|
||||||
|
const CONTRAST_PAIRS = [ |
||||||
|
{ fg: "#ffffff", bg: "#53514e", label: "Белый на тёмном серо-голубом (073M)" }, |
||||||
|
{ fg: "#ffffff", bg: "#1b2e5d", label: "Белый на тёмно-синем (050M)" }, |
||||||
|
{ fg: "#ffffff", bg: "#432f1e", label: "Белый на тёмно-коричневом (080M)" }, |
||||||
|
{ fg: "#ffffff", bg: "#00818c", label: "Белый на среднем бирюзовом (066M)" }, |
||||||
|
{ fg: "#111827", bg: "#0089c3", label: "Тёмный текст на основном бирюзовом (053M)" }, |
||||||
|
{ fg: "#111827", bg: "#a8885c", label: "Тёмный текст на бежевом (081M)" }, |
||||||
|
{ fg: "#432f1e", bg: "#a8885c", label: "Тёмно-коричневый на бежевом (форма, 080M/081M)" }, |
||||||
|
]; |
||||||
|
|
||||||
|
/* ─── Компоненты ───────────────────────────────────────────────────── */ |
||||||
|
function CopyBadge({ value, label }: { value: string; label: string }) { |
||||||
|
const [copied, setCopied] = useState(false); |
||||||
|
const copy = useCallback(() => { |
||||||
|
navigator.clipboard.writeText(value); |
||||||
|
setCopied(true); |
||||||
|
setTimeout(() => setCopied(false), 1500); |
||||||
|
}, [value]); |
||||||
|
return ( |
||||||
|
<button |
||||||
|
onClick={copy} |
||||||
|
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono transition-colors" |
||||||
|
style={{ |
||||||
|
background: copied ? "#d1fae5" : "#f3f4f6", |
||||||
|
color: copied ? "#065f46" : "#374151", |
||||||
|
border: "1px solid", |
||||||
|
borderColor: copied ? "#6ee7b7" : "#e5e7eb", |
||||||
|
}} |
||||||
|
title={`Скопировать ${label}`} |
||||||
|
> |
||||||
|
{copied ? "✓" : "⎘"} {value} |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function ColorCard({ color }: { color: typeof BRAND_COLORS[0] }) { |
||||||
|
const { r, g, b } = hexToRgb(color.hex); |
||||||
|
const { h, s, l } = rgbToHsl(r, g, b); |
||||||
|
const isLight = l > 55; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
{/* Свотч */} |
||||||
|
<div |
||||||
|
className="h-32 flex items-end justify-between px-4 pb-3" |
||||||
|
style={{ background: color.hex }} |
||||||
|
> |
||||||
|
{color.oracal !== "—" && ( |
||||||
|
<span |
||||||
|
className="text-xs font-semibold px-2 py-0.5 rounded" |
||||||
|
style={{ |
||||||
|
background: isLight ? "rgba(0,0,0,0.15)" : "rgba(255,255,255,0.2)", |
||||||
|
color: isLight ? "rgba(0,0,0,0.8)" : "rgba(255,255,255,0.9)", |
||||||
|
}} |
||||||
|
> |
||||||
|
Oracal {color.oracal} |
||||||
|
</span> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Информация */} |
||||||
|
<div className="p-4" style={{ background: "var(--bb-content-bg)" }}> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
{color.name} |
||||||
|
</p> |
||||||
|
<p className="text-xs mb-3" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{color.usage} |
||||||
|
</p> |
||||||
|
|
||||||
|
{/* Коды */} |
||||||
|
<div className="flex flex-wrap gap-1.5"> |
||||||
|
<CopyBadge value={color.hex.toUpperCase()} label="HEX" /> |
||||||
|
<CopyBadge value={`rgb(${r}, ${g}, ${b})`} label="RGB" /> |
||||||
|
<CopyBadge value={`hsl(${h}, ${s}%, ${l}%)`} label="HSL" /> |
||||||
|
<CopyBadge value={color.cssVar} label="CSS var" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function ContrastRow({ pair }: { pair: typeof CONTRAST_PAIRS[0] }) { |
||||||
|
const ratio = contrastRatio(pair.fg, pair.bg); |
||||||
|
const aa = ratio >= 4.5; |
||||||
|
const aaa = ratio >= 7; |
||||||
|
const aaLarge = ratio >= 3; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="flex items-center gap-4 p-4 rounded-xl border" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
{/* Превью */} |
||||||
|
<div |
||||||
|
className="w-32 shrink-0 h-12 rounded-lg flex items-center justify-center text-sm font-medium" |
||||||
|
style={{ background: pair.bg, color: pair.fg }} |
||||||
|
> |
||||||
|
Aa Бб Вв |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Описание */} |
||||||
|
<div className="flex-1 min-w-0"> |
||||||
|
<p className="text-sm font-medium truncate" style={{ color: "var(--bb-text)" }}> |
||||||
|
{pair.label} |
||||||
|
</p> |
||||||
|
<div className="flex gap-1 mt-1 items-center"> |
||||||
|
<span className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{pair.fg} / {pair.bg} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Ratio и бейджи */} |
||||||
|
<div className="shrink-0 flex items-center gap-3"> |
||||||
|
<span className="font-mono font-semibold text-sm" style={{ color: "var(--bb-text)" }}> |
||||||
|
{ratio}:1 |
||||||
|
</span> |
||||||
|
<div className="flex gap-1"> |
||||||
|
{[ |
||||||
|
{ label: "AA", pass: aa }, |
||||||
|
{ label: "AAA", pass: aaa }, |
||||||
|
{ label: "AA large", pass: aaLarge }, |
||||||
|
].map(({ label, pass }) => ( |
||||||
|
<span |
||||||
|
key={label} |
||||||
|
className="text-[10px] font-semibold px-1.5 py-0.5 rounded" |
||||||
|
style={{ |
||||||
|
background: pass ? "#d1fae5" : "#fee2e2", |
||||||
|
color: pass ? "#065f46" : "#991b1b", |
||||||
|
}} |
||||||
|
> |
||||||
|
{pass ? "✓" : "✕"} {label} |
||||||
|
</span> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function WebColorCard({ color }: { color: typeof WEB_COLORS[0] }) { |
||||||
|
const { r, g, b } = hexToRgb(color.hex); |
||||||
|
const { h, s, l } = rgbToHsl(r, g, b); |
||||||
|
const isLight = l > 60; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="h-24 flex items-end justify-between px-4 pb-3" |
||||||
|
style={{ background: color.hex }} |
||||||
|
> |
||||||
|
<span |
||||||
|
className="text-xs font-semibold px-2 py-0.5 rounded" |
||||||
|
style={{ |
||||||
|
background: isLight ? "rgba(0,0,0,0.12)" : "rgba(255,255,255,0.18)", |
||||||
|
color: isLight ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{color.group} |
||||||
|
</span> |
||||||
|
<span |
||||||
|
className="text-xs px-2 py-0.5 rounded font-mono" |
||||||
|
style={{ |
||||||
|
background: isLight ? "rgba(0,0,0,0.12)" : "rgba(255,255,255,0.18)", |
||||||
|
color: isLight ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)", |
||||||
|
}} |
||||||
|
> |
||||||
|
×{color.count} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div className="p-4" style={{ background: "var(--bb-content-bg)" }}> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
{color.name} |
||||||
|
</p> |
||||||
|
<p className="text-xs mb-3" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{color.usage} |
||||||
|
</p> |
||||||
|
<div className="flex flex-wrap gap-1.5"> |
||||||
|
<CopyBadge value={color.hex.toUpperCase()} label="HEX" /> |
||||||
|
<CopyBadge value={`rgb(${r}, ${g}, ${b})`} label="RGB" /> |
||||||
|
<CopyBadge value={`hsl(${h}, ${s}%, ${l}%)`} label="HSL" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Экспорт токенов ──────────────────────────────────────────────── */ |
||||||
|
function exportTokens() { |
||||||
|
const tokens: Record<string, Record<string, unknown>> = { colors: {} }; |
||||||
|
BRAND_COLORS.forEach(c => { |
||||||
|
const key = c.oracal !== "—" ? `brand-${c.oracal.toLowerCase()}` : "brand-white"; |
||||||
|
const { r, g, b } = hexToRgb(c.hex); |
||||||
|
const { h, s, l } = rgbToHsl(r, g, b); |
||||||
|
tokens.colors[key] = { |
||||||
|
oracal: c.oracal, |
||||||
|
name: c.name, |
||||||
|
hex: c.hex.toUpperCase(), |
||||||
|
rgb: `rgb(${r}, ${g}, ${b})`, |
||||||
|
hsl: `hsl(${h}, ${s}%, ${l}%)`, |
||||||
|
cssVar: c.cssVar, |
||||||
|
}; |
||||||
|
}); |
||||||
|
const blob = new Blob([JSON.stringify(tokens, null, 2)], { type: "application/json" }); |
||||||
|
const url = URL.createObjectURL(blob); |
||||||
|
const a = document.createElement("a"); |
||||||
|
a.href = url; a.download = "oclinica-brand-tokens.json"; a.click(); |
||||||
|
URL.revokeObjectURL(url); |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── LLM spec text ────────────────────────────────────────────────── */ |
||||||
|
const LLM_COLORS_TEXT = `# ЦВЕТА — LLM-СПЕЦИФИКАЦИЯ
|
||||||
|
# Клиника ухо, горло, нос им. проф. Е.Н.Оленевой |
||||||
|
# docs/LLM_CONTEXT.md · /foundation/colors · v2.2 · 2026-03-22 |
||||||
|
|
||||||
|
ФИРМЕННЫЕ ЦВЕТА (Oracal) — точные значения из каталога |
||||||
|
Oracal | Название | HEX | RGB | CSS-переменная | Применение |
||||||
|
053M | Основной бирюзовый | #0089C3 | rgb(0,137,195) | --brand-053m | Акцент, CTA-кнопки, иконки, активные состояния |
||||||
|
073M | Тёмный серо-голубой | #53514E | rgb(83,81,78) | --brand-073m | Тёмный фон, хедер, заголовки навигации |
||||||
|
066M | Средний бирюзовый | #00818C | rgb(0,129,140) | --brand-066m | Вторичные акценты, фоны секций |
||||||
|
050M | Тёмно-синий | #1B2E5D | rgb(27,46,93) | --brand-050m | Наружная реклама, полиграфия, заголовки |
||||||
|
081M | Бежевый | #A8885C | rgb(168,136,92) | --brand-081m | Форма сотрудников, тёплые акценты |
||||||
|
080M | Тёмно-коричневый | #432F1E | rgb(67,47,30) | --brand-080m | Текст на бежевом, логотип на форме |
||||||
|
— | Белый | #FFFFFF | rgb(255,255,255) | --brand-white | Фон, инвертированный текст, логотип на тёмных |
||||||
|
|
||||||
|
ЦВЕТА САЙТА oclinica.ru (CSS: clinic_bootstrap_mobile/css/style.css) |
||||||
|
Название | HEX | Группа | × | Применение |
||||||
|
Бежевый | #BF9975 | Акценты | 12| Тёплый акцент, фоны, рамки, текст |
||||||
|
Серо-бирюзовый | #60959C | Акценты | 7 | Холодный акцент, ссылки |
||||||
|
Бирюзовый | #63BAC3 | Акценты | 4 | Фоны акцентных блоков, иконки |
||||||
|
Бирюзовый средний | #52B4BD | Акценты | 4 | Вторичные цветовые акценты |
||||||
|
Коралловый | #FFA39C | Акценты | 2 | CTA-кнопки («Запишите меня!») |
||||||
|
Основной текст | #464646 | Текст | 3 | Цвет основного текста сайта |
||||||
|
Второстепенный текст | #949290 | Текст | 4 | Подписи, второстепенный контент |
||||||
|
Светло-кремовый Hero | #F9F4E7 | Фоны | 1 | Фон Hero-баннера страниц разделов |
||||||
|
Светло-бирюзовый фон | #B8E6ED | Фоны | 1 | Фон формы записи («Будьте здоровы!») |
||||||
|
Кремовый фон | #E9E4D4 | Фоны | 1 | Тёплые фоны кнопок-пилюль, вторичные секции |
||||||
|
Светло-жёлтый фон | #EEF4D1 | Фоны | 1 | Фон карточек отзывов |
||||||
|
Светло-зелёный фон | #F2FEE6 | Фоны | 1 | Фон секции новостей |
||||||
|
|
||||||
|
СООТВЕТСТВИЕ ORACAL → САЙТ (цифровая адаптация плёночных цветов) |
||||||
|
053M #0089C3 → #63BAC3 (Oracal ярко-синий; сайт светлее и голубее) |
||||||
|
073M #53514E → #60959C (Oracal тёмно-серый; сайт серо-бирюзовый — значительное расхождение) |
||||||
|
066M #00818C → #52B4BD (Oracal тёмный бирюзовый; сайт светлее) |
||||||
|
081M #A8885C → #BF9975 (Oracal тёплый охристый; сайт прохладнее и светлее) |
||||||
|
050M #1B2E5D → не найден в CSS сайта |
||||||
|
080M #432F1E → не найден в CSS сайта |
||||||
|
|
||||||
|
КОНТРАСТНОСТЬ WCAG 2.1 |
||||||
|
#FFFFFF / #53514E | 7.9:1 | AA PASS | AAA PASS |
||||||
|
#FFFFFF / #1B2E5D | 13.2:1 | AA PASS | AAA PASS |
||||||
|
#FFFFFF / #432F1E | 12.6:1 | AA PASS | AAA PASS |
||||||
|
#FFFFFF / #00818C | 4.7:1 | AA PASS | AAA FAIL |
||||||
|
#111827 / #0089C3 | 4.7:1 | AA PASS | AAA FAIL |
||||||
|
#111827 / #A8885C | 5.5:1 | AA PASS | AAA FAIL |
||||||
|
#432F1E / #A8885C | 3.8:1 | AA FAIL | AAA FAIL | только крупный текст ≥18pt |
||||||
|
|
||||||
|
ПРАВИЛА |
||||||
|
✓ Только цвета из фирменной палитры |
||||||
|
✓ Digital → цвета сайта; оффлайн → коды Oracal |
||||||
|
✓ Текст на цветном фоне: минимум WCAG AA (4.5:1) |
||||||
|
✓ Белый текст на: 073M (#53514E), 066M (#00818C), 050M (#1B2E5D), 080M (#432F1E) |
||||||
|
✓ Тёмный текст (#111827) на: 053M (#0089C3), 081M (#A8885C) |
||||||
|
✕ Произвольные цвета вне фирменной палитры |
||||||
|
✕ Изменение насыщенности / оттенка фирменных цветов |
||||||
|
✕ Тёплые и холодные акценты рядом без нейтрального разделителя`.trim();
|
||||||
|
|
||||||
|
/* ─── Страница ─────────────────────────────────────────────────────── */ |
||||||
|
export default function ColorsPage() { |
||||||
|
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)" }} |
||||||
|
> |
||||||
|
Фундамент → 1.3 |
||||||
|
</p> |
||||||
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Цвета |
||||||
|
</h1> |
||||||
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Фирменная цветовая палитра основана на кодах плёнки Oracal. HEX-значения |
||||||
|
подобраны как ближайшие эквиваленты. Для точной печати используйте коды Oracal. |
||||||
|
</p> |
||||||
|
|
||||||
|
<div className="mt-4 flex items-center justify-between"> |
||||||
|
<div |
||||||
|
className="px-4 py-3 rounded-lg border text-sm flex items-center gap-2" |
||||||
|
style={{ borderColor: "#bae6fd", background: "#f0f9ff", color: "#075985" }} |
||||||
|
> |
||||||
|
<span>ℹ</span> |
||||||
|
<span> |
||||||
|
HEX-значения рассчитаны по точным RGB из каталога Oracal. |
||||||
|
Для оффлайн-носителей используйте официальные коды Oracal. |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<button |
||||||
|
onClick={exportTokens} |
||||||
|
className="ml-4 shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors" |
||||||
|
style={{ |
||||||
|
background: "var(--brand-053m)", |
||||||
|
color: "#ffffff", |
||||||
|
}} |
||||||
|
> |
||||||
|
↓ Экспорт JSON |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* 1. Палитра */} |
||||||
|
<section className="mb-12"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Фирменные цвета |
||||||
|
</h2> |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Нажмите на значение, чтобы скопировать его в буфер обмена. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> |
||||||
|
{BRAND_COLORS.map(c => ( |
||||||
|
<ColorCard key={c.oracal} color={c} /> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* 2. Контрастность */} |
||||||
|
<section className="mb-12"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Контрастность пар (WCAG) |
||||||
|
</h2> |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Проверка соответствия требованиям доступности для основных комбинаций цветов. |
||||||
|
AA = 4.5:1 для обычного текста, 3:1 для крупного (>18pt). |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col gap-3"> |
||||||
|
{CONTRAST_PAIRS.map(pair => ( |
||||||
|
<ContrastRow key={pair.label} pair={pair} /> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* 3. Цвета с сайта */} |
||||||
|
<section className="mb-12"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Цвета с сайта |
||||||
|
</h2> |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Реальные цвета, используемые на сайте oclinica.ru. Извлечены из CSS темы сайта. |
||||||
|
Число применений показано в правом углу карточки. |
||||||
|
</p> |
||||||
|
<div |
||||||
|
className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg text-xs w-fit font-mono" |
||||||
|
style={{ background: "var(--bb-sidebar-bg)", color: "var(--bb-text-muted)", border: "1px solid var(--bb-border)" }} |
||||||
|
> |
||||||
|
<span style={{ color: "var(--brand-053m)" }}>CSS</span> |
||||||
|
perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> |
||||||
|
{WEB_COLORS.map(c => ( |
||||||
|
<WebColorCard key={c.hex} color={c} /> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* 4. Соответствие */} |
||||||
|
<section className="mb-12"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Соответствие: Oracal → Сайт |
||||||
|
</h2> |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Сравнение фирменной палитры (Oracal, брендбук) с реальными цветами, применёнными на сайте. |
||||||
|
Расхождения ожидаемы — цифровая адаптация плёночных цветов под экран. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col gap-3"> |
||||||
|
{COLOR_MAPPING.map((row, i) => ( |
||||||
|
<div |
||||||
|
key={i} |
||||||
|
className="flex items-center gap-3 p-4 rounded-xl border" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-content-bg)" }} |
||||||
|
> |
||||||
|
{/* Левая сторона — Oracal */} |
||||||
|
<div className="flex items-center gap-3 w-56 shrink-0"> |
||||||
|
{row.brand ? ( |
||||||
|
<> |
||||||
|
<div |
||||||
|
className="w-10 h-10 rounded-lg shrink-0 border" |
||||||
|
style={{ background: row.brand.hex, borderColor: "var(--bb-border)" }} |
||||||
|
/> |
||||||
|
<div> |
||||||
|
<p className="text-xs font-semibold font-mono" style={{ color: "var(--bb-text)" }}> |
||||||
|
{row.brand.hex.toUpperCase()} |
||||||
|
</p> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Oracal {row.brand.oracal} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<div |
||||||
|
className="w-10 h-10 rounded-lg shrink-0 flex items-center justify-center text-lg border" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} |
||||||
|
> |
||||||
|
— |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Стрелка */} |
||||||
|
<div className="shrink-0 text-sm font-bold" style={{ color: "var(--bb-text-muted)" }}>→</div> |
||||||
|
|
||||||
|
{/* Правая сторона — Сайт */} |
||||||
|
<div className="flex items-center gap-3 w-56 shrink-0"> |
||||||
|
{row.web ? ( |
||||||
|
<> |
||||||
|
<div |
||||||
|
className="w-10 h-10 rounded-lg shrink-0 border" |
||||||
|
style={{ background: row.web.hex, borderColor: "var(--bb-border)" }} |
||||||
|
/> |
||||||
|
<div> |
||||||
|
<p className="text-xs font-semibold font-mono" style={{ color: "var(--bb-text)" }}> |
||||||
|
{row.web.hex.toUpperCase()} |
||||||
|
</p> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{row.web.name} · ×{row.web.count} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<div |
||||||
|
className="w-10 h-10 rounded-lg shrink-0 flex items-center justify-center text-lg border" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} |
||||||
|
> |
||||||
|
— |
||||||
|
</div> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>Нет на сайте</p> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Примечание */} |
||||||
|
<p className="flex-1 text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{row.note} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* 5. Применение */} |
||||||
|
<section className="mb-10"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Правила применения |
||||||
|
</h2> |
||||||
|
</div> |
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> |
||||||
|
{[ |
||||||
|
{ |
||||||
|
icon: "✓", |
||||||
|
color: "#d1fae5", |
||||||
|
textColor: "#065f46", |
||||||
|
title: "Используйте фирменные цвета", |
||||||
|
text: "Только цвета из палитры обеспечивают узнаваемость бренда на всех носителях.", |
||||||
|
}, |
||||||
|
{ |
||||||
|
icon: "✓", |
||||||
|
color: "#d1fae5", |
||||||
|
textColor: "#065f46", |
||||||
|
title: "Соблюдайте контрастность", |
||||||
|
text: "Текст на цветном фоне должен соответствовать минимум AA по WCAG 2.1.", |
||||||
|
}, |
||||||
|
{ |
||||||
|
icon: "✕", |
||||||
|
color: "#fee2e2", |
||||||
|
textColor: "#991b1b", |
||||||
|
title: "Не смешивайте произвольно", |
||||||
|
text: "Не используйте фирменные цвета с посторонними цветами без согласования.", |
||||||
|
}, |
||||||
|
{ |
||||||
|
icon: "✕", |
||||||
|
color: "#fee2e2", |
||||||
|
textColor: "#991b1b", |
||||||
|
title: "Не изменяйте насыщенность", |
||||||
|
text: "Осветление, затемнение или изменение оттенка недопустимо без необходимости.", |
||||||
|
}, |
||||||
|
].map(item => ( |
||||||
|
<div |
||||||
|
key={item.title} |
||||||
|
className="flex gap-3 p-4 rounded-xl border" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<span |
||||||
|
className="shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" |
||||||
|
style={{ background: item.color, color: item.textColor }} |
||||||
|
> |
||||||
|
{item.icon} |
||||||
|
</span> |
||||||
|
<div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
{item.title} |
||||||
|
</p> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{item.text} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* 6. LLM-спецификация */} |
||||||
|
<section className="mb-8"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
LLM-спецификация |
||||||
|
</h2> |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Машиночитаемые данные раздела для использования AI-ассистентами при разработке |
||||||
|
дизайна, макетов и кода. Нажмите «Скопировать» чтобы получить полный текст. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<LlmBlock |
||||||
|
path="/foundation/colors" |
||||||
|
version="v2.2" |
||||||
|
specText={LLM_COLORS_TEXT} |
||||||
|
> |
||||||
|
{/* Фирменные цвета */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Фирменные цвета (Oracal)" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Oracal", "HEX", "RGB", "CSS-переменная", "Применение"]} |
||||||
|
rows={BRAND_COLORS.map(c => { |
||||||
|
const { r, g, b } = hexToRgb(c.hex); |
||||||
|
return [ |
||||||
|
<span key="o" className="flex items-center gap-1.5"> |
||||||
|
<span className="w-3 h-3 rounded-sm shrink-0 border inline-block" style={{ background: c.hex, borderColor: "var(--bb-border)" }} /> |
||||||
|
{c.oracal} |
||||||
|
</span>, |
||||||
|
<span key="h" style={{ color: "var(--bb-text)" }}>{c.hex.toUpperCase()}</span>, |
||||||
|
`rgb(${r},${g},${b})`, |
||||||
|
<span key="v" style={{ color: "var(--brand-073m)" }}>{c.cssVar}</span>, |
||||||
|
c.usage, |
||||||
|
]; |
||||||
|
})} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Цвета сайта */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Цвета сайта oclinica.ru" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Название", "HEX", "Группа", "×", "Применение"]} |
||||||
|
rows={WEB_COLORS.map(c => [ |
||||||
|
<span key="n" className="flex items-center gap-1.5"> |
||||||
|
<span className="w-3 h-3 rounded-sm shrink-0 border inline-block" style={{ background: c.hex, borderColor: "var(--bb-border)" }} /> |
||||||
|
{c.name} |
||||||
|
</span>, |
||||||
|
<span key="h" style={{ color: "var(--bb-text)" }}>{c.hex.toUpperCase()}</span>, |
||||||
|
c.group, |
||||||
|
String(c.count), |
||||||
|
c.usage, |
||||||
|
])} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Контрастность */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Контрастность WCAG 2.1" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Пара (fg / bg)", "Ratio", "AA (4.5:1)", "AAA (7:1)", "AA large (3:1)"]} |
||||||
|
rows={CONTRAST_PAIRS.map(p => { |
||||||
|
const ratio = contrastRatio(p.fg, p.bg); |
||||||
|
const aa = ratio >= 4.5, aaa = ratio >= 7, aal = ratio >= 3; |
||||||
|
const badge = (pass: boolean) => ( |
||||||
|
<span key={String(pass)} style={{ color: pass ? "#059669" : "#dc2626", fontWeight: 700 }}> |
||||||
|
{pass ? "PASS" : "FAIL"} |
||||||
|
</span> |
||||||
|
); |
||||||
|
return [ |
||||||
|
<span key="pair" className="flex items-center gap-1.5"> |
||||||
|
<span className="w-3 h-3 rounded-sm border inline-block shrink-0" style={{ background: p.bg, borderColor: "var(--bb-border)" }} /> |
||||||
|
{p.fg} / {p.bg} |
||||||
|
</span>, |
||||||
|
`${ratio}:1`, |
||||||
|
badge(aa), |
||||||
|
badge(aaa), |
||||||
|
badge(aal), |
||||||
|
]; |
||||||
|
})} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Правила */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Правила применения" /> |
||||||
|
<LlmRules rules={[ |
||||||
|
{ ok: true, text: "Только цвета из фирменной палитры" }, |
||||||
|
{ ok: true, text: "Digital → цвета сайта; оффлайн → коды Oracal" }, |
||||||
|
{ ok: true, text: "Текст на цветном фоне: минимум WCAG AA (4.5:1)" }, |
||||||
|
{ ok: true, text: "Белый текст на: 073M (#53514E), 066M (#00818C), 050M (#1B2E5D), 080M (#432F1E)" }, |
||||||
|
{ ok: true, text: "Тёмный текст (#111827) на: 053M (#0089C3), 081M (#A8885C)" }, |
||||||
|
{ ok: false, text: "Произвольные цвета вне фирменной палитры" }, |
||||||
|
{ ok: false, text: "Изменение насыщенности / оттенка фирменных цветов" }, |
||||||
|
{ ok: false, text: "Тёплые + холодные акценты рядом без разделителя" }, |
||||||
|
]} /> |
||||||
|
</div> |
||||||
|
</LlmBlock> |
||||||
|
</section> |
||||||
|
|
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,560 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Типографика. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
/* ─── Шкала типографики ────────────────────────────────────────────── */ |
||||||
|
const DIN_SCALE = [ |
||||||
|
{ token: "h1", size: "40px / 2.5rem", weight: "700", lh: "1.2", sample: "Клиника ухо, горло, нос" }, |
||||||
|
{ token: "h2", size: "32px / 2rem", weight: "700", lh: "1.25", sample: "Наши специалисты" }, |
||||||
|
{ token: "h3", size: "24px / 1.5rem", weight: "700", lh: "1.3", sample: "Запись на приём" }, |
||||||
|
{ token: "h4", size: "20px / 1.25rem",weight: "700", lh: "1.35", sample: "Расписание врачей" }, |
||||||
|
{ token: "h5", size: "16px / 1rem", weight: "700", lh: "1.4", sample: "Услуги и цены" }, |
||||||
|
{ token: "h6", size: "14px / 0.875rem",weight:"700", lh: "1.4", sample: "Контакты клиники" }, |
||||||
|
]; |
||||||
|
|
||||||
|
const FIRA_SCALE = [ |
||||||
|
{ token: "h1", size: "40px / 2.5rem", weight: "600", lh: "1.2", sample: "Клиника ухо, горло, нос" }, |
||||||
|
{ token: "h2", size: "32px / 2rem", weight: "600", lh: "1.25", sample: "Наши специалисты" }, |
||||||
|
{ token: "h3", size: "24px / 1.5rem", weight: "600", lh: "1.3", sample: "Запись на приём" }, |
||||||
|
{ token: "h4", size: "20px / 1.25rem", weight: "500", lh: "1.35", sample: "Расписание врачей" }, |
||||||
|
{ token: "h5", size: "16px / 1rem", weight: "500", lh: "1.4", sample: "Услуги и цены" }, |
||||||
|
{ token: "h6", size: "14px / 0.875rem",weight: "500", lh: "1.4", sample: "Контакты клиники" }, |
||||||
|
{ token: "body", size: "16px / 1rem", weight: "400", lh: "1.6", sample: "ЛОР-клиника предоставляет полный спектр услуг по диагностике и лечению заболеваний уха, горла и носа." }, |
||||||
|
{ token: "body-sm", size: "14px / 0.875rem",weight: "400", lh: "1.6", sample: "Запись по телефону или через форму на сайте. Работаем без выходных." }, |
||||||
|
{ token: "caption", size: "12px / 0.75rem", weight: "400", lh: "1.5", sample: "Лицензия № ЛО-77-01-018234 от 12.01.2022" }, |
||||||
|
{ token: "label", size: "12px / 0.75rem", weight: "500", lh: "1.4", sample: "СПЕЦИАЛИЗАЦИЯ ВРАЧА" }, |
||||||
|
{ token: "overline", size: "10px / 0.625rem",weight: "600", lh: "1.4", sample: "ФУНДАМЕНТ → 1.4" }, |
||||||
|
]; |
||||||
|
|
||||||
|
/* ─── LLM spec text ────────────────────────────────────────────────── */ |
||||||
|
const LLM_TYPOGRAPHY_TEXT = `# ТИПОГРАФИКА — LLM-СПЕЦИФИКАЦИЯ
|
||||||
|
# Клиника ухо, горло, нос им. проф. Е.Н.Оленевой |
||||||
|
# docs/LLM_CONTEXT.md · /foundation/typography · v2.0 · 2026-03-22 |
||||||
|
|
||||||
|
ШРИФТЫ БРЕНДА |
||||||
|
Шрифт | Тип | Применение | CSS |
||||||
|
DINPro | Бренд | Оффлайн, физические носители (бейджи, таблички, транспорт, форма) | font-family: 'DINPro', Arial, sans-serif |
||||||
|
Fira Sans| Веб | Сайт, цифровые материалы, брендбук | font-family: 'Fira Sans', sans-serif; Google Fonts; weights: 300/400/500/600 |
||||||
|
|
||||||
|
ПРАВИЛО ВЫБОРА: носитель цифровой → Fira Sans; физический/печатный → DINPro |
||||||
|
|
||||||
|
DINPro — ШКАЛА (оффлайн) |
||||||
|
Стиль | font-size | rem | weight | line-height |
||||||
|
h1 | 40px | 2.5rem | 700 | 1.20 |
||||||
|
h2 | 32px | 2rem | 700 | 1.25 |
||||||
|
h3 | 24px | 1.5rem | 700 | 1.30 |
||||||
|
h4 | 20px | 1.25rem | 700 | 1.35 |
||||||
|
h5 | 16px | 1rem | 700 | 1.40 |
||||||
|
h6 | 14px | 0.875rem | 700 | 1.40 |
||||||
|
|
||||||
|
Fira Sans — ШКАЛА (веб) |
||||||
|
Стиль | font-size | rem | weight | line-height | letter-spacing |
||||||
|
h1 | 40px | 2.5rem | 600 | 1.20 | -0.025em |
||||||
|
h2 | 32px | 2rem | 600 | 1.25 | -0.020em |
||||||
|
h3 | 24px | 1.5rem | 600 | 1.30 | -0.010em |
||||||
|
h4 | 20px | 1.25rem | 500 | 1.35 | 0em |
||||||
|
h5 | 16px | 1rem | 500 | 1.40 | 0em |
||||||
|
h6 | 14px | 0.875rem | 500 | 1.40 | +0.010em |
||||||
|
body | 16px | 1rem | 400 | 1.60 | 0em |
||||||
|
body-sm | 14px | 0.875rem | 400 | 1.60 | 0em |
||||||
|
caption | 12px | 0.75rem | 400 | 1.50 | +0.020em |
||||||
|
label | 12px | 0.75rem | 500 | 1.40 | +0.030em |
||||||
|
overline | 10px | 0.625rem | 600 | 1.40 | +0.100em (uppercase) |
||||||
|
|
||||||
|
ПРИМЕНЕНИЕ ПО НОСИТЕЛЮ |
||||||
|
Носитель | Шрифт |
||||||
|
Сайт, цифровые материалы, брендбук | Fira Sans |
||||||
|
Форма сотрудников, бейджи | DINPro |
||||||
|
Вывески, таблички, навигация | DINPro |
||||||
|
Брендирование транспорта | DINPro |
||||||
|
Визитки, листовки, полиграфия | DINPro |
||||||
|
Telegram-бот, пуш-уведомления | Fira Sans (системный) |
||||||
|
|
||||||
|
ПРАВИЛА |
||||||
|
✓ H1 — только один на странице |
||||||
|
✓ Не пропускать уровни заголовков (h1 → h2 → h3) |
||||||
|
✓ Минимальный размер текста на экране: 12px |
||||||
|
✓ Кириллица → Fira Sans (не DINPro) |
||||||
|
✓ Fira Sans: доступные веса 300 / 400 / 500 / 600 |
||||||
|
✕ DINPro на сайте без явного согласования дизайнера |
||||||
|
✕ Light (300) для текста < 14px |
||||||
|
✕ Смешивать DINPro и Fira Sans на одном носителе`.trim();
|
||||||
|
|
||||||
|
/* ─── Компоненты ───────────────────────────────────────────────────── */ |
||||||
|
function Section({ |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section 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 UsageBadge({ label, color }: { label: string; color: string }) { |
||||||
|
return ( |
||||||
|
<span |
||||||
|
className="inline-block px-2 py-0.5 rounded text-xs font-medium" |
||||||
|
style={{ background: color === "teal" ? "#e0f5f4" : "#fef3c7", color: color === "teal" ? "var(--brand-073m)" : "#92400e" }} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</span> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function TypeRow({ |
||||||
|
token, |
||||||
|
size, |
||||||
|
weight, |
||||||
|
lh, |
||||||
|
sample, |
||||||
|
fontFamily, |
||||||
|
}: { |
||||||
|
token: string; |
||||||
|
size: string; |
||||||
|
weight: string; |
||||||
|
lh: string; |
||||||
|
sample: string; |
||||||
|
fontFamily: string; |
||||||
|
}) { |
||||||
|
const [sizeVal] = size.split(" / "); |
||||||
|
const pxSize = parseInt(sizeVal); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="border-t py-4 grid gap-4" |
||||||
|
style={{ |
||||||
|
borderColor: "var(--bb-border)", |
||||||
|
gridTemplateColumns: "80px 1fr 140px", |
||||||
|
alignItems: "start", |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* Токен */} |
||||||
|
<div className="pt-1"> |
||||||
|
<span |
||||||
|
className="font-mono text-xs px-2 py-0.5 rounded" |
||||||
|
style={{ background: "#f3f4f6", color: "#374151" }} |
||||||
|
> |
||||||
|
{token} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Образец */} |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
fontFamily, |
||||||
|
fontSize: `${Math.min(pxSize, 28)}px`, |
||||||
|
fontWeight: weight, |
||||||
|
lineHeight: lh, |
||||||
|
color: "var(--bb-text)", |
||||||
|
wordBreak: "break-word", |
||||||
|
}} |
||||||
|
> |
||||||
|
{sample} |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Метаданные */} |
||||||
|
<div className="pt-1 space-y-1 text-right"> |
||||||
|
<p className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{size} |
||||||
|
</p> |
||||||
|
<p className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
w{weight} · lh{lh} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Страница ─────────────────────────────────────────────────────── */ |
||||||
|
export default function TypographyPage() { |
||||||
|
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)" }} |
||||||
|
> |
||||||
|
Фундамент → 1.4 |
||||||
|
</p> |
||||||
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Типографика |
||||||
|
</h1> |
||||||
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Бренд использует два шрифта с чётким разделением по контексту применения. |
||||||
|
Никогда не смешивайте их в одном носителе без необходимости. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Карточки шрифтов */} |
||||||
|
<section className="mb-12"> |
||||||
|
<h2 className="text-xl font-semibold mb-6" style={{ color: "var(--bb-text)" }}> |
||||||
|
Шрифты бренда |
||||||
|
</h2> |
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> |
||||||
|
{/* DINPro */} |
||||||
|
<div |
||||||
|
className="rounded-xl border p-6" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<div className="flex items-start justify-between mb-3"> |
||||||
|
<div> |
||||||
|
<p className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
||||||
|
DINPro |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Бренд-шрифт · не Google Fonts |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<UsageBadge label="Оффлайн · Бренд" color="yellow" /> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className="text-4xl mb-4 font-bold" |
||||||
|
style={{ fontFamily: "'DINPro', Arial, sans-serif", color: "var(--bb-text)" }} |
||||||
|
> |
||||||
|
Aa Бб Вв |
||||||
|
</div> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Применяется на: форме сотрудников, бейджах, вывесках, транспорте, |
||||||
|
полиграфии. Лицензионный шрифт — передаётся дизайнером. |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-3 font-mono px-2 py-1 rounded inline-block" |
||||||
|
style={{ background: "#f3f4f6", color: "#374151" }}> |
||||||
|
--font-brand |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Fira Sans */} |
||||||
|
<div |
||||||
|
className="rounded-xl border p-6" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<div className="flex items-start justify-between mb-3"> |
||||||
|
<div> |
||||||
|
<p className="font-semibold text-base" style={{ color: "var(--bb-text)" }}> |
||||||
|
Fira Sans |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Веб-шрифт · Google Fonts |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<UsageBadge label="Веб · Цифровой" color="teal" /> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className="text-4xl mb-4 font-semibold" |
||||||
|
style={{ fontFamily: "var(--font-fira-sans)", color: "var(--bb-text)" }} |
||||||
|
> |
||||||
|
Aa Бб Вв |
||||||
|
</div> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Применяется на: сайте oclinica.ru, в цифровом брендбуке, онлайн-материалах. |
||||||
|
Подключён через next/font/google (кириллица + латиница). |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-3 font-mono px-2 py-1 rounded inline-block" |
||||||
|
style={{ background: "#f3f4f6", color: "#374151" }}> |
||||||
|
--font-web |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* Таблица применения */} |
||||||
|
<section className="mb-12"> |
||||||
|
<h2 className="text-xl font-semibold mb-4" style={{ color: "var(--bb-text)" }}> |
||||||
|
Правило применения |
||||||
|
</h2> |
||||||
|
<div |
||||||
|
className="overflow-hidden rounded-xl border" |
||||||
|
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-5 py-3 font-medium" |
||||||
|
style={{ color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
{h} |
||||||
|
</th> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{[ |
||||||
|
["Сайт, цифровые материалы, брендбук", "Fira Sans"], |
||||||
|
["Форма сотрудников, бейджи", "DINPro"], |
||||||
|
["Вывески, таблички, навигация", "DINPro"], |
||||||
|
["Брендирование транспорта", "DINPro"], |
||||||
|
["Визитки, листовки, полиграфия", "DINPro"], |
||||||
|
["Telegram-бот, пуш-уведомления", "Fira Sans (системный)"], |
||||||
|
].map(([media, font]) => ( |
||||||
|
<tr |
||||||
|
key={media} |
||||||
|
className="border-t" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text)" }}>{media}</td> |
||||||
|
<td className="px-5 py-3 font-medium" style={{ color: "var(--brand-073m)" }}> |
||||||
|
{font} |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
{/* DINPro Scale */} |
||||||
|
<Section |
||||||
|
title="DINPro — шкала заголовков" |
||||||
|
subtitle="Бренд-шрифт. Используется в оффлайн-носителях. Отображается с системным фоллбэком Arial, если шрифт не установлен." |
||||||
|
> |
||||||
|
<div> |
||||||
|
{DIN_SCALE.map(row => ( |
||||||
|
<TypeRow |
||||||
|
key={row.token} |
||||||
|
{...row} |
||||||
|
fontFamily="'DINPro', 'DIN Pro', Arial, sans-serif" |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Fira Sans Scale */} |
||||||
|
<Section |
||||||
|
title="Fira Sans — полная шкала" |
||||||
|
subtitle="Веб-шрифт. Используется на сайте и в цифровых материалах. Загружается через Google Fonts." |
||||||
|
> |
||||||
|
<div> |
||||||
|
{FIRA_SCALE.map(row => ( |
||||||
|
<TypeRow |
||||||
|
key={row.token} |
||||||
|
{...row} |
||||||
|
fontFamily="var(--font-fira-sans, 'Fira Sans', sans-serif)" |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Живой пример */} |
||||||
|
<Section |
||||||
|
title="Живой пример — блок контента" |
||||||
|
subtitle="Типичная комбинация заголовка и тела текста на сайте." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="rounded-xl border p-8" |
||||||
|
style={{ |
||||||
|
borderColor: "var(--bb-border)", |
||||||
|
background: "var(--bb-sidebar-bg)", |
||||||
|
fontFamily: "var(--font-fira-sans, 'Fira Sans', sans-serif)", |
||||||
|
}} |
||||||
|
> |
||||||
|
<p |
||||||
|
className="text-xs font-semibold uppercase tracking-widest mb-3" |
||||||
|
style={{ color: "var(--brand-053m)", letterSpacing: "0.1em" }} |
||||||
|
> |
||||||
|
Наши специалисты |
||||||
|
</p> |
||||||
|
<h2 |
||||||
|
className="mb-4" |
||||||
|
style={{ fontSize: 28, fontWeight: 600, lineHeight: 1.3, color: "var(--bb-text)" }} |
||||||
|
> |
||||||
|
Запись к ЛОР-врачу<br />без очереди и ожидания |
||||||
|
</h2> |
||||||
|
<p |
||||||
|
className="mb-4 max-w-xl" |
||||||
|
style={{ fontSize: 16, fontWeight: 400, lineHeight: 1.6, color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
В нашей клинике работают специалисты высшей категории с опытом от 15 лет. |
||||||
|
Диагностика и лечение заболеваний уха, горла, носа и смежных органов. |
||||||
|
</p> |
||||||
|
<p |
||||||
|
style={{ fontSize: 12, fontWeight: 400, lineHeight: 1.5, color: "#9ca3af" }} |
||||||
|
> |
||||||
|
Лицензия № ЛО-77-01-018234 · Москва, ул. Примерная, д. 1 |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Типографика сайта — фактическое состояние 23.03.2026 */} |
||||||
|
<Section |
||||||
|
title="Типографика сайта — факт на 23.03.2026" |
||||||
|
subtitle="Реальные размеры и стили, задокументированные по perm.oclinica.ru/lor. Источник: скриншот + CSS темы clinic_bootstrap_mobile." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="overflow-hidden rounded-xl border" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<table className="w-full text-sm"> |
||||||
|
<thead> |
||||||
|
<tr style={{ background: "var(--bb-sidebar-bg)" }}> |
||||||
|
{["Элемент / блок", "Шрифт", "Размер", "Weight", "Цвет", "Пример"].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> |
||||||
|
{[ |
||||||
|
["H1 страницы (над Hero)", "Fira Sans", "~28–30px", "700", "#bf9975", "ЛОР Клиника ухо, горло, нос…"], |
||||||
|
["H2 секции", "Fira Sans", "~30px", "700", "#111827", "Приём ведут опытные ЛОР врачи"], |
||||||
|
["Заголовок Hero-баннера", "Fira Sans", "~18px", "700", "#111827", "ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ…"], |
||||||
|
["Статистика (цифра+текст)", "Fira Sans", "~18px", "700", "#60959c", "Ежедневно 27 ЛОР врачей…"], |
||||||
|
["Подзаголовок секции", "Fira Sans", "~14px", "400", "#374151", "Фундаментальная теоретическая…"], |
||||||
|
["Пункт checklist (ключ)", "Fira Sans", "13px", "700", "#111827", "БЕЗОПАСНО"], |
||||||
|
["Пункт checklist (описание)", "Fira Sans", "13px", "400", "#374151", "оперируют хирурги с 15-летним…"], |
||||||
|
["Имя врача в карточке", "Fira Sans", "12px", "500", "#60959c", "Макарова Людмила Германовна"], |
||||||
|
["Специализация врача", "Fira Sans", "11px", "400", "#374151", "ЛОР врач, сурдолог"], |
||||||
|
].map(([el, font, size, w, color, example]) => ( |
||||||
|
<tr key={el} className="border-t" style={{ borderColor: "var(--bb-border)" }}> |
||||||
|
<td className="px-4 py-3 font-medium text-xs" style={{ color: "var(--bb-text)" }}>{el}</td> |
||||||
|
<td className="px-4 py-3 text-xs font-mono" style={{ color: "var(--brand-073m)" }}>{font}</td> |
||||||
|
<td className="px-4 py-3 text-xs font-mono" style={{ color: "var(--bb-text)" }}>{size}</td> |
||||||
|
<td className="px-4 py-3 text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>{w}</td> |
||||||
|
<td className="px-4 py-3"> |
||||||
|
<span className="flex items-center gap-1.5"> |
||||||
|
<span className="w-3 h-3 rounded-sm inline-block border shrink-0" |
||||||
|
style={{ background: color, borderColor: "var(--bb-border)" }} /> |
||||||
|
<span className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>{color}</span> |
||||||
|
</span> |
||||||
|
</td> |
||||||
|
<td className="px-4 py-3 text-xs" style={{ color: "var(--bb-text-muted)" }}>{example}</td> |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* LLM-спецификация */} |
||||||
|
<section className="mb-8"> |
||||||
|
<div className="mb-6"> |
||||||
|
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
LLM-спецификация |
||||||
|
</h2> |
||||||
|
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Машиночитаемые данные раздела для использования AI-ассистентами при разработке |
||||||
|
дизайна, макетов и кода. Нажмите «Скопировать» чтобы получить полный текст. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<LlmBlock |
||||||
|
path="/foundation/typography" |
||||||
|
version="v2.0" |
||||||
|
specText={LLM_TYPOGRAPHY_TEXT} |
||||||
|
> |
||||||
|
{/* Шрифты */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Шрифты бренда" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Шрифт", "Тип", "Применение", "CSS font-family"]} |
||||||
|
rows={[ |
||||||
|
[ |
||||||
|
<span key="d" style={{ color: "var(--bb-text)", fontWeight: 600 }}>DINPro</span>, |
||||||
|
"Бренд", |
||||||
|
"Оффлайн, физические носители (бейджи, таблички, транспорт, форма)", |
||||||
|
"'DINPro', Arial, sans-serif", |
||||||
|
], |
||||||
|
[ |
||||||
|
<span key="f" style={{ color: "var(--bb-text)", fontWeight: 600 }}>Fira Sans</span>, |
||||||
|
"Веб", |
||||||
|
"Сайт, цифровые материалы, брендбук", |
||||||
|
"'Fira Sans', sans-serif · Google Fonts · 300/400/500/600", |
||||||
|
], |
||||||
|
]} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* DINPro шкала */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="DINPro — шкала (оффлайн)" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Стиль", "font-size", "rem", "weight", "line-height"]} |
||||||
|
rows={DIN_SCALE.map(r => { |
||||||
|
const [px, rem] = r.size.split(" / "); |
||||||
|
return [r.token, px, rem, r.weight, r.lh]; |
||||||
|
})} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Fira Sans шкала */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Fira Sans — шкала (веб)" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Стиль", "font-size", "rem", "weight", "line-height", "letter-spacing"]} |
||||||
|
rows={[ |
||||||
|
["h1", "40px", "2.5rem", "600", "1.20", "-0.025em"], |
||||||
|
["h2", "32px", "2rem", "600", "1.25", "-0.020em"], |
||||||
|
["h3", "24px", "1.5rem", "600", "1.30", "-0.010em"], |
||||||
|
["h4", "20px", "1.25rem", "500", "1.35", "0em"], |
||||||
|
["h5", "16px", "1rem", "500", "1.40", "0em"], |
||||||
|
["h6", "14px", "0.875rem", "500", "1.40", "+0.010em"], |
||||||
|
["body", "16px", "1rem", "400", "1.60", "0em"], |
||||||
|
["body-sm", "14px", "0.875rem", "400", "1.60", "0em"], |
||||||
|
["caption", "12px", "0.75rem", "400", "1.50", "+0.020em"], |
||||||
|
["label", "12px", "0.75rem", "500", "1.40", "+0.030em"], |
||||||
|
["overline", "10px", "0.625rem", "600", "1.40", "+0.100em"], |
||||||
|
]} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Применение */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Применение по носителю" /> |
||||||
|
<LlmTable |
||||||
|
headers={["Носитель", "Шрифт"]} |
||||||
|
rows={[ |
||||||
|
["Сайт, цифровые материалы, брендбук", <span key="fs" style={{ color: "var(--brand-073m)" }}>Fira Sans</span>], |
||||||
|
["Форма сотрудников, бейджи", <span key="d1" style={{ color: "var(--brand-073m)" }}>DINPro</span>], |
||||||
|
["Вывески, таблички, навигация", <span key="d2" style={{ color: "var(--brand-073m)" }}>DINPro</span>], |
||||||
|
["Брендирование транспорта", <span key="d3" style={{ color: "var(--brand-073m)" }}>DINPro</span>], |
||||||
|
["Визитки, листовки, полиграфия", <span key="d4" style={{ color: "var(--brand-073m)" }}>DINPro</span>], |
||||||
|
["Telegram-бот, пуш-уведомления", <span key="fs2" style={{ color: "var(--brand-073m)" }}>Fira Sans (системный)</span>], |
||||||
|
]} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Правила */} |
||||||
|
<div className="space-y-2"> |
||||||
|
<LlmSection title="Правила применения" /> |
||||||
|
<LlmRules rules={[ |
||||||
|
{ ok: true, text: "H1 — только один на странице" }, |
||||||
|
{ ok: true, text: "Не пропускать уровни заголовков (h1 → h2 → h3)" }, |
||||||
|
{ ok: true, text: "Минимальный размер текста на экране: 12px" }, |
||||||
|
{ ok: true, text: "Кириллица → Fira Sans (не DINPro)" }, |
||||||
|
{ ok: true, text: "Fira Sans: доступные веса 300 / 400 / 500 / 600" }, |
||||||
|
{ ok: false, text: "DINPro на сайте без явного согласования дизайнера" }, |
||||||
|
{ ok: false, text: "Light (300) для текста < 14px" }, |
||||||
|
{ ok: false, text: "Смешивать DINPro и Fira Sans на одном носителе" }, |
||||||
|
]} /> |
||||||
|
</div> |
||||||
|
</LlmBlock> |
||||||
|
</section> |
||||||
|
|
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,194 @@ |
|||||||
|
@import "tailwindcss"; |
||||||
|
|
||||||
|
/* ─── Бренд-токены О!Клиника ─────────────────────────────────────────── */ |
||||||
|
/* Цвета уточняются в Sprint 2 по таблице Oracal */ |
||||||
|
:root { |
||||||
|
/* Фирменные цвета — точные RGB из каталога Oracal */ |
||||||
|
--brand-053m: #0089c3; /* Основной бирюзовый (Oracal 053M) · rgb(0,137,195) */ |
||||||
|
--brand-073m: #53514e; /* Тёмный серо-голубой (Oracal 073M) · rgb(83,81,78) */ |
||||||
|
--brand-066m: #00818c; /* Средний бирюзовый (Oracal 066M) · rgb(0,129,140) */ |
||||||
|
--brand-050m: #1b2e5d; /* Тёмно-синий, наружная реклама (Oracal 050M) · rgb(27,46,93) */ |
||||||
|
--brand-081m: #a8885c; /* Бежевый (Oracal 081M) · rgb(168,136,92) */ |
||||||
|
--brand-080m: #432f1e; /* Тёмно-коричневый (Oracal 080M) · 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; |
||||||
|
--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; |
||||||
|
|
||||||
|
/* Шрифты */ |
||||||
|
--font-brand: 'DINPro', 'DIN Pro', Arial, sans-serif; |
||||||
|
--font-web: 'Fira Sans', sans-serif; |
||||||
|
--font-mono: 'JetBrains Mono', 'Courier New', monospace; |
||||||
|
} |
||||||
|
|
||||||
|
@theme inline { |
||||||
|
--color-background: #ffffff; |
||||||
|
--color-foreground: var(--bb-text); |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
font-family: var(--font-web); |
||||||
|
background: var(--bb-content-bg); |
||||||
|
color: var(--bb-text); |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Анимации ───────────────────────────────────────────────── */ |
||||||
|
@keyframes bb-spin { |
||||||
|
to { transform: rotate(360deg); } |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Кнопки (Sprint 3) ──────────────────────────────────────── */ |
||||||
|
.bb-btn { |
||||||
|
font-family: var(--font-web); |
||||||
|
font-weight: 500; |
||||||
|
display: inline-flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
gap: 7px; |
||||||
|
cursor: pointer; |
||||||
|
transition: filter 0.15s, opacity 0.15s; |
||||||
|
white-space: nowrap; |
||||||
|
line-height: 1; |
||||||
|
text-decoration: none; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
} |
||||||
|
.bb-btn:hover:not(:disabled) { filter: brightness(0.9); } |
||||||
|
.bb-btn:active:not(:disabled) { filter: brightness(0.82); } |
||||||
|
.bb-btn:disabled { cursor: not-allowed; opacity: 0.5; } |
||||||
|
.bb-btn:focus-visible { outline: 2px solid var(--brand-053m); outline-offset: 2px; } |
||||||
|
|
||||||
|
/* Размеры — только padding и font-size, radius задаётся вариантом */ |
||||||
|
.bb-btn-sm { font-size: 13px; padding: 4px 11px; border: 1px solid transparent; } |
||||||
|
.bb-btn-md { font-size: 14px; padding: 8px 16px; border: 1px solid transparent; } |
||||||
|
.bb-btn-lg { font-size: 18px; padding: 10px 24px; border: 1px solid transparent; font-weight: bold; } |
||||||
|
|
||||||
|
/* Варианты — цвета и радиус по реальному сайту oclinica.ru */ |
||||||
|
|
||||||
|
/* primary — коралловая форм-кнопка «Запишите меня!» — #FFA39C */ |
||||||
|
.bb-btn-primary { |
||||||
|
background: #FFA39C; |
||||||
|
color: #fff; |
||||||
|
border-color: #FF847B; |
||||||
|
border-radius: 7px; |
||||||
|
font-weight: bold; |
||||||
|
box-shadow: 0px 0px 5px rgba(0,0,0,0.4), 0px 3px 5px rgba(0,0,0,0.25); |
||||||
|
} |
||||||
|
|
||||||
|
/* outline — белая с бежевой рамкой «Записаться на прием» */ |
||||||
|
.bb-btn-outline { |
||||||
|
background: #fff; |
||||||
|
color: #BF9975; |
||||||
|
border-color: #BF9975; |
||||||
|
border-radius: 7px; |
||||||
|
} |
||||||
|
|
||||||
|
/* teal — бирюзовая «Позвонить» */ |
||||||
|
.bb-btn-teal { |
||||||
|
background: #60959c; |
||||||
|
color: #fff; |
||||||
|
border-color: transparent; |
||||||
|
border-radius: 7px; |
||||||
|
} |
||||||
|
|
||||||
|
/* pill — кремовая таблетка «Заказать звонок» */ |
||||||
|
.bb-btn-pill { |
||||||
|
background: #e9e4d4; |
||||||
|
color: #333; |
||||||
|
border-color: #d5cfbd; |
||||||
|
border-radius: 25px; |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Форм-контролы (Sprint 3) ───────────────────────────────── */ |
||||||
|
.bb-input, |
||||||
|
.bb-textarea, |
||||||
|
.bb-select { |
||||||
|
font-family: var(--font-web); |
||||||
|
font-size: 14px; |
||||||
|
color: var(--bb-text); |
||||||
|
background: #fff; |
||||||
|
border: 1px solid #ccc; |
||||||
|
border-radius: 4px; |
||||||
|
padding: 10px 12px; |
||||||
|
width: 100%; |
||||||
|
transition: border-color 0.15s, box-shadow 0.15s; |
||||||
|
outline: none; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
} |
||||||
|
.bb-input { height: 50px; } |
||||||
|
.bb-input:focus, |
||||||
|
.bb-textarea:focus, |
||||||
|
.bb-select:focus { |
||||||
|
border-color: var(--brand-053m); |
||||||
|
box-shadow: 0 0 0 3px rgba(0, 137, 195, 0.2); |
||||||
|
} |
||||||
|
.bb-input.bb-error, |
||||||
|
.bb-textarea.bb-error, |
||||||
|
.bb-select.bb-error { border-color: #dc2626; } |
||||||
|
.bb-input.bb-error:focus, |
||||||
|
.bb-textarea.bb-error:focus, |
||||||
|
.bb-select.bb-error:focus { box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); } |
||||||
|
.bb-input:disabled, |
||||||
|
.bb-textarea:disabled, |
||||||
|
.bb-select:disabled { opacity: 0.5; cursor: not-allowed; background: var(--bb-sidebar-bg); } |
||||||
|
|
||||||
|
.bb-textarea { resize: vertical; min-height: 100px; } |
||||||
|
.bb-select { |
||||||
|
cursor: pointer; |
||||||
|
appearance: none; |
||||||
|
height: 50px; |
||||||
|
padding: 10px 36px 10px 10px; |
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-position: right 10px center; |
||||||
|
} |
||||||
|
|
||||||
|
.bb-checkbox, |
||||||
|
.bb-radio { |
||||||
|
width: 16px; |
||||||
|
height: 16px; |
||||||
|
cursor: pointer; |
||||||
|
accent-color: var(--brand-073m); |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Карточки (Sprint 4) ────────────────────────────────────── */ |
||||||
|
.bb-news-card:hover { |
||||||
|
background: #eef4d1 !important; |
||||||
|
box-shadow: 0 0 16px 0 #9e9e9a; |
||||||
|
} |
||||||
|
.bb-service-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); } |
||||||
|
|
||||||
|
/* ─── Навигация сайта (Sprint 5) ────────────────────────────── */ |
||||||
|
.bb-nav-link:hover { color: var(--brand-053m) !important; } |
||||||
|
|
||||||
|
/* ─── Тумблер (Sprint 3) ──────────────────────────────────────── */ |
||||||
|
.bb-toggle-track { |
||||||
|
display: inline-flex; |
||||||
|
align-items: center; |
||||||
|
width: 44px; |
||||||
|
height: 24px; |
||||||
|
border-radius: 12px; |
||||||
|
padding: 2px; |
||||||
|
cursor: pointer; |
||||||
|
transition: background 0.2s; |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
.bb-toggle-thumb { |
||||||
|
width: 20px; |
||||||
|
height: 20px; |
||||||
|
border-radius: 50%; |
||||||
|
background: #fff; |
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); |
||||||
|
transition: transform 0.2s; |
||||||
|
} |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { Fira_Sans } from "next/font/google"; |
||||||
|
import "./globals.css"; |
||||||
|
import { Sidebar } from "@/components/layout/Sidebar"; |
||||||
|
|
||||||
|
const firaSans = Fira_Sans({ |
||||||
|
subsets: ["latin", "cyrillic"], |
||||||
|
weight: ["300", "400", "500", "600"], |
||||||
|
variable: "--font-fira-sans", |
||||||
|
display: "swap", |
||||||
|
}); |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Цифровой брендбук | Клиника ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
description: "Интерактивный брендбук — Living Styleguide oclinica.ru", |
||||||
|
}; |
||||||
|
|
||||||
|
export default function RootLayout({ |
||||||
|
children, |
||||||
|
}: { |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<html lang="ru" className={firaSans.variable}> |
||||||
|
<body className="flex h-screen overflow-hidden bg-white"> |
||||||
|
<Sidebar /> |
||||||
|
<main className="flex-1 overflow-y-auto"> |
||||||
|
{children} |
||||||
|
</main> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,205 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import Image from "next/image"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Бейджи | Брендбук О!Клиника", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section 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> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default function BadgesPage() { |
||||||
|
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)" }} |
||||||
|
> |
||||||
|
Оффлайн элементы → 6.2 |
||||||
|
</p> |
||||||
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Бейджи сотрудников |
||||||
|
</h1> |
||||||
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Именные бейджи для идентификации сотрудников клиники. Размер 70×30 мм, |
||||||
|
магнитное крепление. Белый фон, чёрный текст. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Фотографии */} |
||||||
|
<Section |
||||||
|
title="Образцы бейджей" |
||||||
|
subtitle="Фотографии реальных бейджей из брендбука клиники." |
||||||
|
> |
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> |
||||||
|
{/* Лицевая сторона */} |
||||||
|
<div> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/badges/badge-2.jpeg" |
||||||
|
alt="Лицевая сторона бейджа: Лебединская Елена Александровна, врач оториноларинголог" |
||||||
|
width={690} |
||||||
|
height={347} |
||||||
|
className="w-full object-cover" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Лицевая сторона |
||||||
|
</p> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Белый фон, скруглённые углы. ФИО — крупный шрифт, |
||||||
|
должности — мелкий. Металлическая рамка. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Оборотная сторона */} |
||||||
|
<div> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/badges/badge-1.jpeg" |
||||||
|
alt="Оборотная сторона бейджа с магнитным креплением" |
||||||
|
width={657} |
||||||
|
height={369} |
||||||
|
className="w-full object-cover" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Оборотная сторона |
||||||
|
</p> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Чёрный пластик, магнитное крепление (CAUTION MAGNETIC). |
||||||
|
Не требует проколов в одежде. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Технические параметры */} |
||||||
|
<Section |
||||||
|
title="Технические параметры" |
||||||
|
subtitle="Единый стандарт для всех сотрудников клиники." |
||||||
|
> |
||||||
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6"> |
||||||
|
{[ |
||||||
|
{ label: "Ширина", value: "70 мм" }, |
||||||
|
{ label: "Высота", value: "30 мм" }, |
||||||
|
{ label: "Материал", value: "Металл / ПВХ" }, |
||||||
|
{ label: "Крепление", value: "Магнитное" }, |
||||||
|
].map(({ label, value }) => ( |
||||||
|
<div |
||||||
|
key={label} |
||||||
|
className="rounded-xl border p-4 text-center" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} |
||||||
|
> |
||||||
|
<p className="font-mono text-lg font-semibold" style={{ color: "var(--brand-073m)" }}> |
||||||
|
{value} |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-1" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{label} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Состав текста */} |
||||||
|
<Section |
||||||
|
title="Состав текста на бейдже" |
||||||
|
subtitle="Строгий порядок элементов." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="overflow-hidden rounded-xl border" |
||||||
|
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-5 py-3 font-medium" |
||||||
|
style={{ color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
{h} |
||||||
|
</th> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{[ |
||||||
|
["ФИО", "Фамилия Имя Отчество", "Крупный шрифт, первая строка"], |
||||||
|
["Должность", "Основная должность", "Мелкий шрифт, вторая строка"], |
||||||
|
["Специализация", "Специализация / учёная степень (если есть)", "Мелкий шрифт, третья строка"], |
||||||
|
].map(([el, content, style]) => ( |
||||||
|
<tr |
||||||
|
key={el} |
||||||
|
className="border-t" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<td className="px-5 py-3 font-medium" style={{ color: "var(--bb-text)" }}>{el}</td> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{content}</td> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{style}</td> |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Пример из фото */} |
||||||
|
<div |
||||||
|
className="mt-4 rounded-xl border p-4 flex items-start gap-3" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="w-1.5 self-stretch rounded-full shrink-0" |
||||||
|
style={{ background: "var(--brand-053m)" }} |
||||||
|
/> |
||||||
|
<div> |
||||||
|
<p className="text-xs font-medium mb-1" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Пример из брендбука |
||||||
|
</p> |
||||||
|
<p className="text-sm font-semibold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Лебединская Елена Александровна |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
врач оториноларинголог · ведущий хирург · кандидат медицинских наук |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,243 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import Image from "next/image"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Внутренняя навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section 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> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default function NavigationPage() { |
||||||
|
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)" }} |
||||||
|
> |
||||||
|
Оффлайн элементы → 6.3 |
||||||
|
</p> |
||||||
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Внутренняя навигация |
||||||
|
</h1> |
||||||
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Система навигационных табличек и указателей внутри клиники. |
||||||
|
На оргстекле, наклейки из плёнок Оракл 053M и 073M. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Макеты таблечек */} |
||||||
|
<Section |
||||||
|
title="Макеты навигационных табличек" |
||||||
|
subtitle="Два типа: табличка кабинета с бирюзовым заголовком и карточка врача с логотипом и QR-кодом." |
||||||
|
> |
||||||
|
<div className="max-w-lg"> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/navigation/nav-mockup-signs.jpeg" |
||||||
|
alt="Макеты навигационных табличек: Кабинет 04 с бирюзовым заголовком и карточка врача Лебединской" |
||||||
|
width={620} |
||||||
|
height={570} |
||||||
|
className="w-full object-cover" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Макет из брендбука |
||||||
|
</p> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Табличка «Кабинет 04»: бирюзовый заголовок (Oracal 053M), специализация и ФИО врачей. |
||||||
|
Карточка врача: логотип клиники, имя, должности, QR-код на страницу врача. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Нумерация дверей */} |
||||||
|
<Section |
||||||
|
title="Нумерация кабинетов на дверях" |
||||||
|
subtitle="Номер кабинета размещается непосредственно на двери — крупный шрифт, плёнка Oracal." |
||||||
|
> |
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> |
||||||
|
<div> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/navigation/nav-door-31.png" |
||||||
|
alt="Белая дверь кабинета 31 с крупным номером из плёнки" |
||||||
|
width={770} |
||||||
|
height={963} |
||||||
|
className="w-full object-cover" |
||||||
|
style={{ maxHeight: 420, objectPosition: "top" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Кабинет 31 — белая дверь |
||||||
|
</p> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Крупный номер в верхней части двери. Тёмная плёнка на белом фоне. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/navigation/nav-3.jpeg" |
||||||
|
alt="Серая дверь кабинета 13 с крупным номером" |
||||||
|
width={800} |
||||||
|
height={463} |
||||||
|
className="w-full object-cover" |
||||||
|
style={{ maxHeight: 420, objectPosition: "left" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Кабинет 13 — серая дверь |
||||||
|
</p> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Тёмная дверь с крупным номером. Рядом — табличка кабинета на стене. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Указатели по этажам */} |
||||||
|
<Section |
||||||
|
title="Указатели по этажам" |
||||||
|
subtitle="Навигационные панели в холлах — показывают расположение кабинетов и специализации на каждом этаже." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/navigation/nav-render-p13.jpeg" |
||||||
|
alt="Навигационные панели по этажам: кабинеты 01-06, 21-25, 31-37, 41-45" |
||||||
|
width={2105} |
||||||
|
height={1489} |
||||||
|
className="w-full" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="mt-3 text-sm" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Слева — указатель с этажом и направлением, справа — панель с полным перечнем кабинетов. |
||||||
|
Активный этаж выделяется бирюзовым фоном (Oracal 053M). |
||||||
|
</p> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Технические требования */} |
||||||
|
<Section |
||||||
|
title="Технические требования" |
||||||
|
subtitle="Материалы и цвета для производства." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="overflow-hidden rounded-xl border" |
||||||
|
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-5 py-3 font-medium" |
||||||
|
style={{ color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
{h} |
||||||
|
</th> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{[ |
||||||
|
["Основной материал", "Оргстекло"], |
||||||
|
["Покрытие", "Наклейка из плёнок Oracal"], |
||||||
|
["Шрифт", "DINPro Bold / Regular"], |
||||||
|
["Крепление", "Дистанционные держатели"], |
||||||
|
].map(([param, value]) => ( |
||||||
|
<tr |
||||||
|
key={param} |
||||||
|
className="border-t" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<td className="px-5 py-3 font-medium" style={{ color: "var(--bb-text)" }}> |
||||||
|
{param} |
||||||
|
</td> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{value} |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Цвета Oracal */} |
||||||
|
<Section |
||||||
|
title="Цвета Oracal для навигации" |
||||||
|
subtitle="Допустимые цвета плёнки по коду Oracal." |
||||||
|
> |
||||||
|
<div className="flex gap-6"> |
||||||
|
{[ |
||||||
|
{ code: "053M", hex: "#7ecfca", name: "Заголовок таблички / активный этаж" }, |
||||||
|
{ code: "073M", hex: "#5b7b87", name: "Дополнительный акцент" }, |
||||||
|
].map(c => ( |
||||||
|
<div |
||||||
|
key={c.code} |
||||||
|
className="flex items-center gap-4 rounded-xl border p-4" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="w-12 h-12 rounded-lg shrink-0" |
||||||
|
style={{ background: c.hex }} |
||||||
|
/> |
||||||
|
<div> |
||||||
|
<p className="font-medium text-sm" style={{ color: "var(--bb-text)" }}> |
||||||
|
Oracal {c.code} |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{c.name} |
||||||
|
</p> |
||||||
|
<p className="font-mono text-xs mt-1" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{c.hex} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,331 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import Image from "next/image"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Печатные материалы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section 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 BusinessCardMockup({ side }: { side: "front" | "back" }) { |
||||||
|
/* 90×50 мм → пропорция 9:5. Отображаем 288×160px */ |
||||||
|
if (side === "front") { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden shadow-md" |
||||||
|
style={{ width: 288, height: 160, background: "#5b7b87", flexShrink: 0 }} |
||||||
|
> |
||||||
|
{/* Верхняя бирюзовая полоса */} |
||||||
|
<div style={{ height: 6, background: "#7ecfca" }} /> |
||||||
|
|
||||||
|
<div className="flex h-full"> |
||||||
|
{/* Левая колонка с логотипом */} |
||||||
|
<div |
||||||
|
className="flex items-center justify-center px-6" |
||||||
|
style={{ width: 110, borderRight: "1px solid rgba(255,255,255,0.15)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/logo/logo-transparent.png" |
||||||
|
alt="Логотип" |
||||||
|
width={80} |
||||||
|
height={28} |
||||||
|
className="object-contain" |
||||||
|
style={{ filter: "brightness(0) invert(1)" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Правая колонка с данными */} |
||||||
|
<div className="flex flex-col justify-center px-5 pb-2 gap-1"> |
||||||
|
<p |
||||||
|
style={{ |
||||||
|
fontFamily: "'DINPro', Arial, sans-serif", |
||||||
|
fontSize: 12, |
||||||
|
fontWeight: 700, |
||||||
|
color: "#ffffff", |
||||||
|
}} |
||||||
|
> |
||||||
|
Иванова Анна Викторовна |
||||||
|
</p> |
||||||
|
<p |
||||||
|
style={{ |
||||||
|
fontFamily: "'DINPro', Arial, sans-serif", |
||||||
|
fontSize: 9, |
||||||
|
color: "rgba(255,255,255,0.7)", |
||||||
|
}} |
||||||
|
> |
||||||
|
Врач-оториноларинголог |
||||||
|
</p> |
||||||
|
<div className="mt-2 space-y-0.5"> |
||||||
|
{["+7 (495) 000-00-00", "oclinica.ru"].map(line => ( |
||||||
|
<p |
||||||
|
key={line} |
||||||
|
style={{ |
||||||
|
fontFamily: "'DINPro', Arial, sans-serif", |
||||||
|
fontSize: 8, |
||||||
|
color: "rgba(255,255,255,0.6)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{line} |
||||||
|
</p> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden shadow-md flex items-center justify-center" |
||||||
|
style={{ width: 288, height: 160, background: "#c4a882", flexShrink: 0 }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/logo/logo-transparent.png" |
||||||
|
alt="Логотип на обороте" |
||||||
|
width={140} |
||||||
|
height={50} |
||||||
|
className="object-contain" |
||||||
|
style={{ |
||||||
|
filter: "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)", |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/* Макет листовки */ |
||||||
|
function LeafletMockup() { |
||||||
|
/* А5 = 148×210 мм → пропорция ≈ 1:1.42. Отображаем 180×256px */ |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden shadow-md" |
||||||
|
style={{ width: 180, height: 256, background: "#ffffff", border: "1px solid #e5e7eb", flexShrink: 0 }} |
||||||
|
> |
||||||
|
{/* Шапка */} |
||||||
|
<div |
||||||
|
className="px-4 pt-4 pb-3 flex items-end justify-between" |
||||||
|
style={{ background: "#5b7b87" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/logo/logo-transparent.png" |
||||||
|
alt="Логотип" |
||||||
|
width={90} |
||||||
|
height={32} |
||||||
|
className="object-contain" |
||||||
|
style={{ filter: "brightness(0) invert(1)" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Тело */} |
||||||
|
<div className="px-4 pt-3"> |
||||||
|
{/* Плейсхолдер фото */} |
||||||
|
<div |
||||||
|
className="w-full rounded mb-3" |
||||||
|
style={{ height: 70, background: "#e0f5f4" }} |
||||||
|
/> |
||||||
|
{/* Заголовок */} |
||||||
|
<div |
||||||
|
className="h-3 rounded mb-1.5" |
||||||
|
style={{ background: "#1b4c72", width: "80%" }} |
||||||
|
/> |
||||||
|
<div |
||||||
|
className="h-3 rounded mb-3" |
||||||
|
style={{ background: "#1b4c72", width: "60%" }} |
||||||
|
/> |
||||||
|
{/* Текст-заглушка */} |
||||||
|
{[100, 90, 95, 80].map((w, i) => ( |
||||||
|
<div |
||||||
|
key={i} |
||||||
|
className="h-1.5 rounded mb-1" |
||||||
|
style={{ background: "#e5e7eb", width: `${w}%` }} |
||||||
|
/> |
||||||
|
))} |
||||||
|
{/* CTA-кнопка */} |
||||||
|
<div |
||||||
|
className="mt-4 rounded py-2 text-center" |
||||||
|
style={{ background: "#7ecfca" }} |
||||||
|
> |
||||||
|
<p |
||||||
|
style={{ |
||||||
|
fontFamily: "'DINPro', Arial, sans-serif", |
||||||
|
fontSize: 9, |
||||||
|
fontWeight: 700, |
||||||
|
color: "#ffffff", |
||||||
|
}} |
||||||
|
> |
||||||
|
ЗАПИСАТЬСЯ НА ПРИЁМ |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default function PrintPage() { |
||||||
|
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)" }} |
||||||
|
> |
||||||
|
Оффлайн элементы → 6.5 |
||||||
|
</p> |
||||||
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Печатные материалы |
||||||
|
</h1> |
||||||
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Листовки, визитки и digital-каналы клиники. Все материалы следуют |
||||||
|
единому фирменному стилю с использованием шрифта DINPro и цветов Oracal. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Визитки */} |
||||||
|
<Section |
||||||
|
title="Визитки" |
||||||
|
subtitle="Стандарт 90×50 мм. Двусторонняя печать. Изображены в масштабе." |
||||||
|
> |
||||||
|
<div className="flex flex-wrap gap-6 items-start"> |
||||||
|
<div className="flex flex-col items-center gap-2"> |
||||||
|
<BusinessCardMockup side="front" /> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>Лицевая сторона</p> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col items-center gap-2"> |
||||||
|
<BusinessCardMockup side="back" /> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>Оборотная сторона</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div |
||||||
|
className="mt-6 overflow-hidden rounded-xl border" |
||||||
|
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-5 py-3 font-medium" |
||||||
|
style={{ color: "var(--bb-text-muted)" }}>{h}</th> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{[ |
||||||
|
["Лицевая — логотип", "Белый инвертированный на тёмно-серо-голубом фоне"], |
||||||
|
["Лицевая — ФИО", "DINPro Bold, 10pt, белый"], |
||||||
|
["Лицевая — должность", "DINPro Regular, 8pt, белый 70%"], |
||||||
|
["Лицевая — контакты", "DINPro Regular, 7pt, белый 60%"], |
||||||
|
["Оборотная", "Логотип тёмно-коричневый на бежевом фоне (081M)"], |
||||||
|
["Размер", "90 × 50 мм, 4+4 офсет или цифровая печать"], |
||||||
|
].map(([el, content]) => ( |
||||||
|
<tr key={el} className="border-t" style={{ borderColor: "var(--bb-border)" }}> |
||||||
|
<td className="px-5 py-3 font-medium" style={{ color: "var(--bb-text)" }}>{el}</td> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{content}</td> |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Листовки */} |
||||||
|
<Section |
||||||
|
title="Листовки" |
||||||
|
subtitle="Формат А5 (148×210 мм). Шаблон для акций и информационных материалов." |
||||||
|
> |
||||||
|
<div className="flex gap-8 items-start"> |
||||||
|
<LeafletMockup /> |
||||||
|
<div className="flex-1"> |
||||||
|
<p className="font-medium text-sm mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Структура листовки |
||||||
|
</p> |
||||||
|
<div className="space-y-3"> |
||||||
|
{[ |
||||||
|
{ zone: "Шапка", desc: "Логотип белый на тёмно-серо-голубом (073M)" }, |
||||||
|
{ zone: "Фото", desc: "Фото врача или процедуры, 70% ширины" }, |
||||||
|
{ zone: "Заголовок", desc: "DINPro Bold, тёмно-синий (050M), 18–22pt" }, |
||||||
|
{ zone: "Текст", desc: "DINPro Regular, 9–10pt, серый #374151" }, |
||||||
|
{ zone: "CTA-кнопка", desc: "Основной бирюзовый (053M), DINPro Bold белый" }, |
||||||
|
].map(({ zone, desc }) => ( |
||||||
|
<div key={zone} className="flex gap-3"> |
||||||
|
<span |
||||||
|
className="shrink-0 text-xs font-semibold px-2 py-0.5 rounded mt-0.5" |
||||||
|
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }} |
||||||
|
> |
||||||
|
{zone} |
||||||
|
</span> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>{desc}</p> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Telegram-бот */} |
||||||
|
<Section |
||||||
|
title="Telegram-бот" |
||||||
|
subtitle="Цифровой канал взаимодействия с пациентами." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="rounded-xl border p-6" |
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} |
||||||
|
> |
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> |
||||||
|
{[ |
||||||
|
{ icon: "🤖", title: "Запись на приём", desc: "Выбор врача, даты и времени через бот" }, |
||||||
|
{ icon: "📋", title: "Результаты анализов", desc: "Уведомления и просмотр результатов" }, |
||||||
|
{ icon: "💬", title: "Напоминания", desc: "Напоминание о предстоящем визите" }, |
||||||
|
].map(({ icon, title, desc }) => ( |
||||||
|
<div key={title} className="flex gap-3"> |
||||||
|
<span className="text-2xl shrink-0">{icon}</span> |
||||||
|
<div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
{title} |
||||||
|
</p> |
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{desc}</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className="mt-4 pt-4 border-t text-xs" |
||||||
|
style={{ borderColor: "var(--bb-border)", color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
В Telegram-боте используется системный шрифт Telegram. Логотип и цвета бренда |
||||||
|
применяются в аватаре, приветственном изображении и кнопках меню. |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,228 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import Image from "next/image"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: "Форма сотрудников | Брендбук О!Клиника", |
||||||
|
}; |
||||||
|
|
||||||
|
function Section({ |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
children, |
||||||
|
}: { |
||||||
|
title: string; |
||||||
|
subtitle?: string; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<section 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> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default function UniformPage() { |
||||||
|
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)" }} |
||||||
|
> |
||||||
|
Оффлайн элементы → 6.1 |
||||||
|
</p> |
||||||
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}> |
||||||
|
Форма сотрудников |
||||||
|
</h1> |
||||||
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Фирменная медицинская одежда сотрудников клиники. Два цветовых варианта: |
||||||
|
бежевый с коричневым логотипом и синий с белым логотипом. |
||||||
|
Логотип размещается на левой стороне груди. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Фотографии вариантов */} |
||||||
|
<Section |
||||||
|
title="Варианты формы" |
||||||
|
subtitle="Фотографии реальной формы сотрудников с логотипом клиники." |
||||||
|
> |
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> |
||||||
|
{/* Бежевый вариант */} |
||||||
|
<div> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/uniform/uniform-1.jpeg" |
||||||
|
alt="Бежевая форма сотрудника клиники с коричневым логотипом" |
||||||
|
width={742} |
||||||
|
height={990} |
||||||
|
className="w-full object-cover" |
||||||
|
style={{ maxHeight: 480, objectPosition: "top" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Бежевый вариант |
||||||
|
</p> |
||||||
|
<p className="text-sm mb-3" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Основная форма для медицинского персонала. Логотип — тёмно-коричневый. |
||||||
|
</p> |
||||||
|
<div className="flex gap-2"> |
||||||
|
<div className="flex items-center gap-1.5"> |
||||||
|
<div |
||||||
|
className="w-4 h-4 rounded" |
||||||
|
style={{ background: "#c4a882", border: "1px solid #e5e7eb" }} |
||||||
|
/> |
||||||
|
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Oracal 081M |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>·</span> |
||||||
|
<div className="flex items-center gap-1.5"> |
||||||
|
<div |
||||||
|
className="w-4 h-4 rounded" |
||||||
|
style={{ background: "#5c2e0e", border: "1px solid #e5e7eb" }} |
||||||
|
/> |
||||||
|
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Логотип 080M |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Синий вариант */} |
||||||
|
<div> |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden border mb-4" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<Image |
||||||
|
src="/offline/uniform/uniform-2.jpeg" |
||||||
|
alt="Синяя форма сотрудника клиники с белым логотипом" |
||||||
|
width={580} |
||||||
|
height={773} |
||||||
|
className="w-full object-cover" |
||||||
|
style={{ maxHeight: 480, objectPosition: "top" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}> |
||||||
|
Синий вариант |
||||||
|
</p> |
||||||
|
<p className="text-sm mb-3" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Альтернативный вариант. Логотип — белый инвертированный. |
||||||
|
</p> |
||||||
|
<div className="flex gap-2"> |
||||||
|
<div className="flex items-center gap-1.5"> |
||||||
|
<div |
||||||
|
className="w-4 h-4 rounded" |
||||||
|
style={{ background: "#4a90c4", border: "1px solid #e5e7eb" }} |
||||||
|
/> |
||||||
|
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Синий медицинский |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>·</span> |
||||||
|
<div className="flex items-center gap-1.5"> |
||||||
|
<div |
||||||
|
className="w-4 h-4 rounded" |
||||||
|
style={{ background: "#ffffff", border: "1px solid #e5e7eb" }} |
||||||
|
/> |
||||||
|
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Логотип белый |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Размеры логотипа */} |
||||||
|
<Section |
||||||
|
title="Размеры логотипа для размещения на форме сотрудников" |
||||||
|
subtitle="Логотип располагается на левой стороне груди. Размер зависит от размера одежды." |
||||||
|
> |
||||||
|
<div |
||||||
|
className="overflow-hidden rounded-xl border" |
||||||
|
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-5 py-3 font-medium" |
||||||
|
style={{ color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
{h} |
||||||
|
</th> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{[ |
||||||
|
["До 46 р. включительно", "70 мм", "25,5 мм", "Левая сторона груди"], |
||||||
|
["От 48 р.", "90 мм", "32,8 мм", "Левая сторона груди"], |
||||||
|
].map(([size, w, h, pos]) => ( |
||||||
|
<tr |
||||||
|
key={size} |
||||||
|
className="border-t" |
||||||
|
style={{ borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text)" }}>{size}</td> |
||||||
|
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>{w}</td> |
||||||
|
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>{h}</td> |
||||||
|
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{pos}</td> |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
{/* Правила */} |
||||||
|
<Section title="Правила использования"> |
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> |
||||||
|
{[ |
||||||
|
{ ok: true, text: "Носить комплект в полном составе" }, |
||||||
|
{ ok: true, text: "Поддерживать чистоту и опрятность формы" }, |
||||||
|
{ ok: true, text: "Логотип только в утверждённых цветовых вариантах" }, |
||||||
|
{ ok: false, text: "Носить форму без логотипа" }, |
||||||
|
{ ok: false, text: "Изменять цвет или материал формы" }, |
||||||
|
{ ok: false, text: "Добавлять сторонние нашивки и знаки" }, |
||||||
|
].map(item => ( |
||||||
|
<div |
||||||
|
key={item.text} |
||||||
|
className="flex items-start gap-3 p-4 rounded-lg border" |
||||||
|
style={{ |
||||||
|
borderColor: item.ok ? "#a7f3d0" : "#fecaca", |
||||||
|
background: item.ok ? "#f0fdf4" : "#fff5f5", |
||||||
|
}} |
||||||
|
> |
||||||
|
<span style={{ color: item.ok ? "#16a34a" : "#ef4444" }}> |
||||||
|
{item.ok ? "✓" : "✕"} |
||||||
|
</span> |
||||||
|
<p className="text-sm" style={{ color: item.ok ? "#14532d" : "#7f1d1d" }}> |
||||||
|
{item.text} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</Section> |
||||||
|
|
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
import { redirect } from "next/navigation"; |
||||||
|
|
||||||
|
export default function RootPage() { |
||||||
|
redirect("/foundation/logo"); |
||||||
|
} |
||||||
@ -0,0 +1,173 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import Link from "next/link"; |
||||||
|
import { usePathname } from "next/navigation"; |
||||||
|
|
||||||
|
type NavItem = { |
||||||
|
label: string; |
||||||
|
href: string; |
||||||
|
soon?: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
type NavSection = { |
||||||
|
title: string; |
||||||
|
items: NavItem[]; |
||||||
|
}; |
||||||
|
|
||||||
|
const NAV: NavSection[] = [ |
||||||
|
{ |
||||||
|
title: "Фундамент", |
||||||
|
items: [ |
||||||
|
{ label: "Логотип", href: "/foundation/logo" }, |
||||||
|
{ label: "Цвета", href: "/foundation/colors" }, |
||||||
|
{ label: "Типографика", href: "/foundation/typography" }, |
||||||
|
{ label: "Иконография", href: "/foundation/icons", soon: true }, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: "Компоненты", |
||||||
|
items: [ |
||||||
|
{ label: "Кнопки", href: "/components/buttons" }, |
||||||
|
{ label: "Форм-контролы", href: "/components/forms" }, |
||||||
|
{ label: "Карточки", href: "/components/cards" }, |
||||||
|
{ label: "Шапка / Навигация", href: "/components/navigation" }, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: "Блоки", |
||||||
|
items: [ |
||||||
|
{ label: "Hero-баннер", href: "/blocks/hero" }, |
||||||
|
{ label: "Вводный текст", href: "/blocks/ceo" }, |
||||||
|
{ label: "Наши врачи", href: "/blocks/doctors" }, |
||||||
|
{ label: "Отзывы", href: "/blocks/reviews" }, |
||||||
|
{ label: "Формы записи", href: "/blocks/contact-forms" }, |
||||||
|
{ label: "Новости", href: "/blocks/news" }, |
||||||
|
{ label: "Подвал / Контакт", href: "/blocks/contact" }, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: "Страницы", |
||||||
|
items: [ |
||||||
|
{ label: "Главная", href: "/pages/home", soon: true }, |
||||||
|
{ label: "Заболевание", href: "/pages/disease", soon: true }, |
||||||
|
{ label: "Все врачи", href: "/pages/doctors", soon: true }, |
||||||
|
{ label: "Врач", href: "/pages/doctor", soon: true }, |
||||||
|
{ label: "Цены", href: "/pages/prices", soon: true }, |
||||||
|
{ label: "Контакты", href: "/pages/contacts", soon: true }, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: "Оффлайн элементы", |
||||||
|
items: [ |
||||||
|
{ label: "Форма сотрудников", href: "/offline/uniform" }, |
||||||
|
{ label: "Бейджи", href: "/offline/badges" }, |
||||||
|
{ label: "Навигация", href: "/offline/navigation" }, |
||||||
|
{ label: "Транспорт", href: "/offline/transport" }, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: "Эксперименты", |
||||||
|
items: [ |
||||||
|
{ label: "Дубли компонентов", href: "/variants/blocks", soon: true }, |
||||||
|
{ label: "Дубли страниц", href: "/variants/pages", soon: true }, |
||||||
|
], |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
export function Sidebar() { |
||||||
|
const pathname = usePathname(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<aside |
||||||
|
className="w-64 shrink-0 h-screen overflow-y-auto border-r flex flex-col" |
||||||
|
style={{ |
||||||
|
background: "var(--bb-sidebar-bg)", |
||||||
|
borderColor: "var(--bb-sidebar-border)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* Логотип брендбука */} |
||||||
|
<div |
||||||
|
className="px-5 py-4 border-b" |
||||||
|
style={{ borderColor: "var(--bb-sidebar-border)" }} |
||||||
|
> |
||||||
|
<p |
||||||
|
className="text-xs font-semibold uppercase tracking-widest mb-0.5" |
||||||
|
style={{ color: "var(--brand-053m)" }} |
||||||
|
> |
||||||
|
Брендбук |
||||||
|
</p> |
||||||
|
<p |
||||||
|
className="text-xs leading-tight" |
||||||
|
style={{ color: "var(--bb-sidebar-text-muted)" }} |
||||||
|
> |
||||||
|
Клиника ухо, горло, нос |
||||||
|
<br /> |
||||||
|
им. проф. Е.Н.Оленевой |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Навигация */} |
||||||
|
<nav className="flex-1 px-3 py-4 space-y-5"> |
||||||
|
{NAV.map((section) => ( |
||||||
|
<div key={section.title}> |
||||||
|
<p |
||||||
|
className="px-2 mb-1 text-[10px] font-semibold uppercase tracking-widest" |
||||||
|
style={{ color: "var(--bb-sidebar-section)" }} |
||||||
|
> |
||||||
|
{section.title} |
||||||
|
</p> |
||||||
|
<ul className="space-y-0.5"> |
||||||
|
{section.items.map((item) => { |
||||||
|
const isActive = pathname === item.href; |
||||||
|
return ( |
||||||
|
<li key={item.href}> |
||||||
|
<Link |
||||||
|
href={item.soon ? "#" : item.href} |
||||||
|
className="flex items-center justify-between px-2 py-1.5 rounded-md text-sm transition-colors" |
||||||
|
style={{ |
||||||
|
color: isActive |
||||||
|
? "var(--brand-073m)" |
||||||
|
: item.soon |
||||||
|
? "var(--bb-sidebar-text-muted)" |
||||||
|
: "var(--bb-sidebar-text)", |
||||||
|
background: isActive |
||||||
|
? "var(--bb-sidebar-active-bg)" |
||||||
|
: "transparent", |
||||||
|
fontWeight: isActive ? 500 : 400, |
||||||
|
cursor: item.soon ? "default" : "pointer", |
||||||
|
}} |
||||||
|
> |
||||||
|
{item.label} |
||||||
|
{item.soon && ( |
||||||
|
<span |
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded" |
||||||
|
style={{ |
||||||
|
background: "#f3f4f6", |
||||||
|
color: "var(--bb-sidebar-section)", |
||||||
|
}} |
||||||
|
> |
||||||
|
скоро |
||||||
|
</span> |
||||||
|
)} |
||||||
|
</Link> |
||||||
|
</li> |
||||||
|
); |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</nav> |
||||||
|
|
||||||
|
{/* Версия */} |
||||||
|
<div |
||||||
|
className="px-5 py-3 border-t text-xs" |
||||||
|
style={{ |
||||||
|
borderColor: "var(--bb-sidebar-border)", |
||||||
|
color: "var(--bb-sidebar-text-muted)", |
||||||
|
}} |
||||||
|
> |
||||||
|
Sprint 5 · v0.5.1 |
||||||
|
</div> |
||||||
|
</aside> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,175 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useState } from "react"; |
||||||
|
|
||||||
|
interface LlmBlockProps { |
||||||
|
/** Путь страницы, например "/foundation/colors" */ |
||||||
|
path: string; |
||||||
|
/** Версия данных, например "v2.1" */ |
||||||
|
version: string; |
||||||
|
/** Плоский текст для копирования */ |
||||||
|
specText: string; |
||||||
|
/** Содержимое блока — таблицы, правила */ |
||||||
|
children: React.ReactNode; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* LlmBlock — переиспользуемый блок LLM-спецификации. |
||||||
|
* Добавляется в конец каждой страницы брендбука, содержащей дизайн-стандарты. |
||||||
|
* Требование: ФТ-03-LLM (TZ.md) · docs/LLM_CONTEXT.md |
||||||
|
*/ |
||||||
|
export function LlmBlock({ path, version, specText, children }: LlmBlockProps) { |
||||||
|
const [copied, setCopied] = useState(false); |
||||||
|
|
||||||
|
function handleCopy(e: React.MouseEvent) { |
||||||
|
e.preventDefault(); |
||||||
|
navigator.clipboard.writeText(specText); |
||||||
|
setCopied(true); |
||||||
|
setTimeout(() => setCopied(false), 2000); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<details |
||||||
|
open |
||||||
|
className="rounded-xl overflow-hidden" |
||||||
|
style={{ |
||||||
|
border: "2px dashed var(--brand-053m)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* Заголовок */} |
||||||
|
<summary |
||||||
|
className="flex items-center justify-between px-5 py-3 cursor-pointer select-none list-none" |
||||||
|
style={{ background: "rgba(126,207,202,0.07)" }} |
||||||
|
> |
||||||
|
<div className="flex items-center gap-2 min-w-0"> |
||||||
|
<span |
||||||
|
className="shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded tracking-wider" |
||||||
|
style={{ background: "var(--brand-053m)", color: "#fff" }} |
||||||
|
> |
||||||
|
LLM |
||||||
|
</span> |
||||||
|
<span className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}> |
||||||
|
LLM-спецификация |
||||||
|
</span> |
||||||
|
<span |
||||||
|
className="text-xs truncate hidden sm:inline" |
||||||
|
style={{ color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
· машиночитаемые данные · docs/LLM_CONTEXT.md |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex items-center gap-3 shrink-0 ml-3"> |
||||||
|
<span |
||||||
|
className="text-[10px] font-mono hidden md:inline" |
||||||
|
style={{ color: "var(--bb-text-muted)" }} |
||||||
|
> |
||||||
|
{path} · {version} |
||||||
|
</span> |
||||||
|
<button |
||||||
|
onClick={handleCopy} |
||||||
|
className="text-xs px-3 py-1 rounded font-medium transition-colors shrink-0" |
||||||
|
style={{ |
||||||
|
background: copied ? "#d1fae5" : "var(--brand-053m)", |
||||||
|
color: copied ? "#065f46" : "#fff", |
||||||
|
}} |
||||||
|
> |
||||||
|
{copied ? "✓ Скопировано" : "Скопировать"} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</summary> |
||||||
|
|
||||||
|
{/* Содержимое */} |
||||||
|
<div |
||||||
|
className="px-5 py-5 space-y-6 border-t" |
||||||
|
style={{ |
||||||
|
borderColor: "var(--brand-053m)", |
||||||
|
borderStyle: "dashed", |
||||||
|
background: "var(--bb-content-bg)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
</details> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── Утилиты для содержимого блока ──────────────────────────── */ |
||||||
|
|
||||||
|
/** Заголовок подсекции внутри LLM-блока */ |
||||||
|
export function LlmSection({ title }: { title: string }) { |
||||||
|
return ( |
||||||
|
<p |
||||||
|
className="text-[10px] font-semibold uppercase tracking-widest pb-1 border-b" |
||||||
|
style={{ color: "var(--bb-text-muted)", borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
{title} |
||||||
|
</p> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** Компактная таблица для LLM-блока */ |
||||||
|
export function LlmTable({ |
||||||
|
headers, |
||||||
|
rows, |
||||||
|
}: { |
||||||
|
headers: string[]; |
||||||
|
rows: (string | React.ReactNode)[][]; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<div className="overflow-x-auto"> |
||||||
|
<table className="w-full text-xs border-collapse font-mono"> |
||||||
|
<thead> |
||||||
|
<tr style={{ background: "var(--bb-sidebar-bg)" }}> |
||||||
|
{headers.map((h) => ( |
||||||
|
<th |
||||||
|
key={h} |
||||||
|
className="text-left px-2 py-1.5 font-medium border whitespace-nowrap" |
||||||
|
style={{ color: "var(--bb-text-muted)", borderColor: "var(--bb-border)" }} |
||||||
|
> |
||||||
|
{h} |
||||||
|
</th> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{rows.map((row, ri) => ( |
||||||
|
<tr key={ri} style={{ borderTop: `1px solid var(--bb-border)` }}> |
||||||
|
{row.map((cell, ci) => ( |
||||||
|
<td |
||||||
|
key={ci} |
||||||
|
className="px-2 py-1 border" |
||||||
|
style={{ |
||||||
|
borderColor: "var(--bb-border)", |
||||||
|
color: "var(--bb-text-muted)", |
||||||
|
maxWidth: "240px", |
||||||
|
}} |
||||||
|
> |
||||||
|
{cell} |
||||||
|
</td> |
||||||
|
))} |
||||||
|
</tr> |
||||||
|
))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** Список правил ✓ / ✕ */ |
||||||
|
export function LlmRules({ rules }: { rules: { ok: boolean; text: string }[] }) { |
||||||
|
return ( |
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-0.5"> |
||||||
|
{rules.map((r) => ( |
||||||
|
<div key={r.text} className="flex items-start gap-1.5 py-0.5 text-xs font-mono"> |
||||||
|
<span |
||||||
|
style={{ color: r.ok ? "#059669" : "#dc2626", fontWeight: 700, flexShrink: 0 }} |
||||||
|
> |
||||||
|
{r.ok ? "✓" : "✕"} |
||||||
|
</span> |
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>{r.text}</span> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
"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> |
||||||
|
); |
||||||
|
} |
||||||
|
); |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useState } from "react"; |
||||||
|
|
||||||
|
export function CodeCopy({ code, lang = "jsx" }: { code: string; lang?: string }) { |
||||||
|
const [copied, setCopied] = useState(false); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div style={{ borderRadius: 8, overflow: "hidden", border: "1px solid var(--bb-border)" }}> |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
display: "flex", |
||||||
|
justifyContent: "space-between", |
||||||
|
alignItems: "center", |
||||||
|
background: "var(--bb-sidebar-bg)", |
||||||
|
padding: "6px 12px", |
||||||
|
borderBottom: "1px solid var(--bb-border)", |
||||||
|
}} |
||||||
|
> |
||||||
|
<span style={{ fontSize: 11, color: "var(--bb-text-muted)", fontFamily: "var(--font-mono)" }}> |
||||||
|
{lang} |
||||||
|
</span> |
||||||
|
<button |
||||||
|
onClick={() => { |
||||||
|
navigator.clipboard.writeText(code); |
||||||
|
setCopied(true); |
||||||
|
setTimeout(() => setCopied(false), 2000); |
||||||
|
}} |
||||||
|
style={{ |
||||||
|
fontSize: 12, |
||||||
|
background: copied ? "#d1fae5" : "var(--brand-053m)", |
||||||
|
color: copied ? "#065f46" : "#fff", |
||||||
|
border: "none", |
||||||
|
borderRadius: 4, |
||||||
|
padding: "3px 10px", |
||||||
|
cursor: "pointer", |
||||||
|
fontWeight: 500, |
||||||
|
fontFamily: "var(--font-web)", |
||||||
|
}} |
||||||
|
> |
||||||
|
{copied ? "✓ Скопировано" : "Скопировать"} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<pre |
||||||
|
style={{ |
||||||
|
margin: 0, |
||||||
|
padding: "12px 16px", |
||||||
|
overflowX: "auto", |
||||||
|
fontSize: 12, |
||||||
|
fontFamily: "var(--font-mono)", |
||||||
|
color: "var(--bb-text-muted)", |
||||||
|
background: "#fff", |
||||||
|
lineHeight: 1.6, |
||||||
|
}} |
||||||
|
> |
||||||
|
<code>{code}</code> |
||||||
|
</pre> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useState } from "react"; |
||||||
|
|
||||||
|
interface ToggleProps { |
||||||
|
defaultChecked?: boolean; |
||||||
|
disabled?: boolean; |
||||||
|
label?: string; |
||||||
|
onChange?: (checked: boolean) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export function Toggle({ defaultChecked = false, disabled = false, label, onChange }: ToggleProps) { |
||||||
|
const [checked, setChecked] = useState(defaultChecked); |
||||||
|
|
||||||
|
function handleToggle() { |
||||||
|
if (disabled) return; |
||||||
|
const next = !checked; |
||||||
|
setChecked(next); |
||||||
|
onChange?.(next); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
display: "inline-flex", |
||||||
|
alignItems: "center", |
||||||
|
gap: 10, |
||||||
|
cursor: disabled ? "not-allowed" : "pointer", |
||||||
|
opacity: disabled ? 0.5 : 1, |
||||||
|
userSelect: "none", |
||||||
|
}} |
||||||
|
onClick={handleToggle} |
||||||
|
role="switch" |
||||||
|
aria-checked={checked} |
||||||
|
tabIndex={disabled ? -1 : 0} |
||||||
|
onKeyDown={(e) => { |
||||||
|
if (e.key === " " || e.key === "Enter") { |
||||||
|
e.preventDefault(); |
||||||
|
handleToggle(); |
||||||
|
} |
||||||
|
}} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="bb-toggle-track" |
||||||
|
style={{ background: checked ? "var(--brand-073m)" : "#d1d5db" }} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="bb-toggle-thumb" |
||||||
|
style={{ transform: checked ? "translateX(20px)" : "translateX(0)" }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
{label && ( |
||||||
|
<span style={{ fontSize: 14, color: "var(--bb-text)", fontFamily: "var(--font-web)" }}> |
||||||
|
{label} |
||||||
|
</span> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config"; |
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals"; |
||||||
|
import nextTs from "eslint-config-next/typescript"; |
||||||
|
|
||||||
|
const eslintConfig = defineConfig([ |
||||||
|
...nextVitals, |
||||||
|
...nextTs, |
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([ |
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**", |
||||||
|
"out/**", |
||||||
|
"build/**", |
||||||
|
"next-env.d.ts", |
||||||
|
]), |
||||||
|
]); |
||||||
|
|
||||||
|
export default eslintConfig; |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
import type { NextConfig } from "next"; |
||||||
|
import path from "path"; |
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === "development"; |
||||||
|
|
||||||
|
const nextConfig: NextConfig = { |
||||||
|
...(isDev && { |
||||||
|
turbopack: { |
||||||
|
root: path.resolve(__dirname, "../.."), |
||||||
|
}, |
||||||
|
}), |
||||||
|
}; |
||||||
|
|
||||||
|
export default nextConfig; |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
{ |
||||||
|
"name": "web", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"scripts": { |
||||||
|
"dev": "next dev", |
||||||
|
"build": "next build", |
||||||
|
"start": "next start", |
||||||
|
"lint": "eslint" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"next": "16.2.1", |
||||||
|
"react": "19.2.4", |
||||||
|
"react-dom": "19.2.4" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@tailwindcss/postcss": "^4", |
||||||
|
"@types/node": "^20", |
||||||
|
"@types/react": "^19", |
||||||
|
"@types/react-dom": "^19", |
||||||
|
"eslint": "^9", |
||||||
|
"eslint-config-next": "16.2.1", |
||||||
|
"tailwindcss": "^4", |
||||||
|
"typescript": "^5" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
ignoredBuiltDependencies: |
||||||
|
- sharp |
||||||
|
- unrs-resolver |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
const config = { |
||||||
|
plugins: { |
||||||
|
"@tailwindcss/postcss": {}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export default config; |
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 762 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 871 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 740 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 145 B |
|
After Width: | Height: | Size: 957 B |
|
After Width: | Height: | Size: 446 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 699 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 141 B |