Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ce176f970 | |||
| 6c5b571884 | |||
| 2b66fb9cc1 | |||
| 3811c579fd | |||
| 2b9319e70d | |||
| d6610a288b | |||
| 77c9733144 | |||
| c1731615ab | |||
| 0855892643 | |||
| 0198947c4e | |||
| 6648f16bcb | |||
| 761347ed85 | |||
| 52acdc98e2 | |||
| 0570b50d9f | |||
| 789f436be1 | |||
| 5298f8c1cd | |||
| ff3f6acc03 | |||
| 39ecd72fde | |||
| a715503ca5 | |||
| 657cc26d3e | |||
| f03599abcf | |||
| 02f561fcd3 | |||
| 1cdf0e7e95 | |||
| 03cceea13d | |||
| 5105310c92 |
@@ -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 @@
|
|||||||
|
@AGENTS.md
|
||||||
@@ -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,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,814 @@
|
|||||||
|
"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: "Кремовый фон", 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: "Кремовый фон", 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 | Подписи, второстепенный контент
|
||||||
|
Светло-бирюзовый фон | #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,483 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Логотип. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
|
};
|
||||||
|
|
||||||
|
function RuleTag({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 LogoCard({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
tag,
|
||||||
|
dark = false,
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
tag: string;
|
||||||
|
dark?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-xl border p-8 mb-4"
|
||||||
|
style={{
|
||||||
|
background: dark ? "var(--brand-073m)" : "#f8f9fa",
|
||||||
|
borderColor: dark ? "transparent" : "var(--bb-border)",
|
||||||
|
minHeight: 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={320}
|
||||||
|
height={130}
|
||||||
|
className="object-contain"
|
||||||
|
style={{ filter: dark ? "brightness(0) invert(1)" : undefined }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
<RuleTag>{tag}</RuleTag>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProhibitedItem({ label }: { label: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-start gap-3 p-4 rounded-lg border"
|
||||||
|
style={{ borderColor: "#fecaca", background: "#fff5f5" }}
|
||||||
|
>
|
||||||
|
<span className="text-red-400 text-lg leading-none mt-0.5">✕</span>
|
||||||
|
<p className="text-sm" style={{ color: "#7f1d1d" }}>{label}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LLM_LOGO_TEXT = `ЛОГОТИП — LLM-спецификация
|
||||||
|
Версия: v1.0 · /foundation/logo
|
||||||
|
|
||||||
|
ФАЙЛЫ
|
||||||
|
PNG с прозрачным фоном: /public/logo/logo-transparent.png
|
||||||
|
SVG/AI: ожидается от дизайнера
|
||||||
|
|
||||||
|
СИМВОЛИКА ЗНАКА
|
||||||
|
Три округлых элемента с равной дистанцией от центра.
|
||||||
|
- Незамкнутая симметрия: символ развития и жизни, а не завершённости
|
||||||
|
- Три элемента: структура ухо-горло-нос, триада равновесия
|
||||||
|
- Отсутствие замкнутой формы: открытость, доступность, человечность
|
||||||
|
|
||||||
|
ЦВЕТОВЫЕ ВАРИАНТЫ
|
||||||
|
Вариант | Фон | CSS-фильтр | Применение
|
||||||
|
Основной | Светлый (#f8f9fa) | нет | Сайт, полиграфия на белом
|
||||||
|
Инвертированный | #5b7b87 (073M) | brightness(0) invert(1) | Хедер, тёмные секции
|
||||||
|
На форме (беж) | #c4a882 (081M) | brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45) | Форма сотрудников (бежевый)
|
||||||
|
На форме (синий)| #1b4c72 (050M) | brightness(0) invert(1) | Форма сотрудников (синий)
|
||||||
|
|
||||||
|
ОХРАННАЯ ЗОНА
|
||||||
|
Минимальный отступ со всех сторон = высота буквы «К» в слове «КЛИНИКА»
|
||||||
|
|
||||||
|
РАЗМЕРЫ НА ФОРМЕ СОТРУДНИКОВ
|
||||||
|
До 46 р. | 70 мм × 25,5 мм | Левая сторона груди
|
||||||
|
От 48 р. | 90 мм × 32,8 мм | Левая сторона груди
|
||||||
|
|
||||||
|
ПРАВИЛА
|
||||||
|
✓ Применять только одобренные цветовые варианты
|
||||||
|
✓ Соблюдать охранную зону
|
||||||
|
✓ Использовать PNG с прозрачным фоном для digital
|
||||||
|
✓ Белый логотип (invert) на тёмных фонах (073M, 050M)
|
||||||
|
✕ Не изменять пропорции или искажать логотип
|
||||||
|
✕ Не изменять цвета элементов логотипа
|
||||||
|
✕ Не добавлять рядом произвольный текст
|
||||||
|
✕ Не размещать на фоне без достаточного контраста
|
||||||
|
✕ Не применять тени, обводки, градиенты`.trim();
|
||||||
|
|
||||||
|
export default function LogoPage() {
|
||||||
|
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.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
|
||||||
|
className="mt-4 px-4 py-3 rounded-lg border text-sm flex items-center gap-2"
|
||||||
|
style={{ borderColor: "#fde68a", background: "#fffbeb", color: "#92400e" }}
|
||||||
|
>
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span>
|
||||||
|
Изображение извлечено из PDF-брендбука. Векторный SVG будет добавлен
|
||||||
|
после передачи исходных файлов от дизайнера.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1. Логотип */}
|
||||||
|
<Section
|
||||||
|
id="hierarchy"
|
||||||
|
title="Логотип"
|
||||||
|
subtitle="Единый логотип клиники. Применяется на всех носителях."
|
||||||
|
>
|
||||||
|
<LogoCard
|
||||||
|
src="/logo/logo-transparent.png"
|
||||||
|
alt="Логотип Клиника ухо, горло, нос им. проф. Е.Н. Оленевой"
|
||||||
|
label="Логотип клиники"
|
||||||
|
description="Применяется в точках контакта с клиентами, на лендингах и сайтах направлений. Применяется для онлайн и оффлайн коммуникаций с клиентами, во внутренней документации."
|
||||||
|
tag="Онлайн · Оффлайн · Документация"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 2. Символика знака */}
|
||||||
|
<Section
|
||||||
|
id="symbol"
|
||||||
|
title="Символика знака"
|
||||||
|
subtitle="О том, что стоит за формой логотипа."
|
||||||
|
>
|
||||||
|
{/* Главный тезис */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl border p-6 mb-6"
|
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||||
|
>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: "var(--bb-text)" }}>
|
||||||
|
В знаке клиники — три округлых элемента, расположенных с равной дистанцией от центра.
|
||||||
|
Это создаёт ощущение симметрии, порядка и движения, но без завершённой формы (звезды или круга).
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
|
{[
|
||||||
|
"Сохраняет баланс и лёгкость",
|
||||||
|
"Намекает на естественность и органику",
|
||||||
|
"Не замыкает символ — оставляет «дыхание», открытость",
|
||||||
|
].map(item => (
|
||||||
|
<div key={item} className="flex items-start gap-2.5">
|
||||||
|
<div
|
||||||
|
className="w-1.5 h-1.5 rounded-full mt-1.5 shrink-0"
|
||||||
|
style={{ background: "var(--brand-053m)" }}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>{item}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Три значения */}
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 mb-6">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
num: "1",
|
||||||
|
title: "Незамкнутая симметрия",
|
||||||
|
text: "Это процесс, а не результат. Знак говорит: «мы развиваемся, мы живые, мы не идеальны, но гармоничны».",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: "2",
|
||||||
|
title: "Три элемента",
|
||||||
|
text: "Классическая структура уха–горла–носа. Триада равновесия, ритм дыхания, символ доверия и открытости.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: "3",
|
||||||
|
title: "Отсутствие замкнутости",
|
||||||
|
text: "Нет барьера — есть приглашение. Открытая форма передаёт заботу, доступность и человечность.",
|
||||||
|
},
|
||||||
|
].map(item => (
|
||||||
|
<div
|
||||||
|
key={item.num}
|
||||||
|
className="rounded-xl border p-5"
|
||||||
|
style={{ borderColor: "var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold mb-3"
|
||||||
|
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }}
|
||||||
|
>
|
||||||
|
{item.num}
|
||||||
|
</div>
|
||||||
|
<p className="font-medium text-sm mb-2" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Почему не звезда */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl border p-5 flex gap-4"
|
||||||
|
style={{ borderColor: "#e0f5f4", background: "#f8fffe" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center shrink-0 mt-0.5"
|
||||||
|
style={{ background: "#e0f5f4" }}
|
||||||
|
>
|
||||||
|
<span className="text-base leading-none">○</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm mb-1.5" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Почему нет законченной звезды
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Звезда — символ завершённости и сакрального порядка. Знак клиники — символ жизни
|
||||||
|
и взаимодействия. Он ближе по духу к живой биоморфной форме (капли, клетки, лепестки),
|
||||||
|
чем к идеальной математической фигуре. Такой дизайн передаёт не «власть формы»,
|
||||||
|
а заботу, мягкость и человечность.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 3. Цветовые варианты */}
|
||||||
|
<Section
|
||||||
|
id="variants"
|
||||||
|
title="Цветовые варианты"
|
||||||
|
subtitle="Логотип существует в нескольких вариантах в зависимости от фона носителя."
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-xl border p-6 mb-3"
|
||||||
|
style={{ background: "#f8f9fa", borderColor: "var(--bb-border)", minHeight: 160 }}
|
||||||
|
>
|
||||||
|
<Image src="/logo/logo-transparent.png" alt="Логотип на светлом фоне"
|
||||||
|
width={220} height={87} className="object-contain" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Основной — на светлом фоне
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-xl p-6 mb-3"
|
||||||
|
style={{ background: "var(--brand-073m)", minHeight: 160 }}
|
||||||
|
>
|
||||||
|
{/* brightness(0) делает всё чёрным, invert(1) — белым */}
|
||||||
|
<Image src="/logo/logo-transparent.png" alt="Логотип инвертированный"
|
||||||
|
width={220} height={87} className="object-contain"
|
||||||
|
style={{ filter: "brightness(0) invert(1)" }} />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Инвертированный — на тёмном фоне
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-xl border p-6 mb-3"
|
||||||
|
style={{ background: "var(--brand-081m)", borderColor: "transparent", minHeight: 160 }}
|
||||||
|
>
|
||||||
|
{/* Коричневый: насыщенный sepia + darkening */}
|
||||||
|
<Image src="/logo/logo-transparent.png" alt="Логотип коричневый на форме"
|
||||||
|
width={220} height={87} className="object-contain"
|
||||||
|
style={{ filter: "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)" }} />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Коричневый — на форме (бежевый костюм)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 3. Охранная зона */}
|
||||||
|
<Section
|
||||||
|
id="clearspace"
|
||||||
|
title="Охранная зона"
|
||||||
|
subtitle="Вокруг логотипа всегда должно быть свободное пространство — не менее высоты буквы «x» в слове «КЛИНИКА»."
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-xl border p-16 relative"
|
||||||
|
style={{ background: "#f8f9fa", borderColor: "var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-8 border-2 border-dashed rounded-lg"
|
||||||
|
style={{ borderColor: "var(--brand-053m)", opacity: 0.5 }} />
|
||||||
|
{["top","bottom","left","right"].map(side => (
|
||||||
|
<div key={side} className={`absolute text-xs font-medium
|
||||||
|
${side==="top" ? "top-2 left-1/2 -translate-x-1/2" : ""}
|
||||||
|
${side==="bottom" ? "bottom-2 left-1/2 -translate-x-1/2" : ""}
|
||||||
|
${side==="left" ? "left-2 top-1/2 -translate-y-1/2" : ""}
|
||||||
|
${side==="right" ? "right-2 top-1/2 -translate-y-1/2" : ""}
|
||||||
|
`} style={{ color: "var(--brand-053m)" }}>x</div>
|
||||||
|
))}
|
||||||
|
<Image src="/logo/logo-transparent.png" alt="Логотип с охранной зоной"
|
||||||
|
width={320} height={126} className="object-contain relative z-10" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Пунктирная рамка — граница охранной зоны. Никакие другие элементы не должны её пересекать.
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 4. Минимальные размеры */}
|
||||||
|
<Section
|
||||||
|
id="sizes"
|
||||||
|
title="Размеры логотипа для размещения на форме сотрудников"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 5. Недопустимые варианты */}
|
||||||
|
<Section
|
||||||
|
id="prohibited"
|
||||||
|
title="Недопустимые варианты использования"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<ProhibitedItem label="Изменять пропорции или искажать логотип" />
|
||||||
|
<ProhibitedItem label="Изменять цвета элементов логотипа" />
|
||||||
|
<ProhibitedItem label="Добавлять рядом произвольный текст" />
|
||||||
|
<ProhibitedItem label="Размещать на фоне без достаточного контраста" />
|
||||||
|
<ProhibitedItem label="Использовать отдельные элементы без остальных" />
|
||||||
|
<ProhibitedItem label="Применять тени, обводки, градиенты" />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 6. Скачать */}
|
||||||
|
<Section id="download" title="Скачать файлы">
|
||||||
|
<div
|
||||||
|
className="rounded-xl border p-6 flex flex-col sm:flex-row items-start sm:items-center gap-4"
|
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Векторные файлы логотипа (SVG, AI, PNG)
|
||||||
|
</p>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Будут доступны после передачи исходных файлов от дизайнера.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button disabled className="px-4 py-2 rounded-lg text-sm font-medium cursor-not-allowed"
|
||||||
|
style={{ background: "#e5e7eb", color: "#9ca3af" }}>
|
||||||
|
Скачать (скоро)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* LLM-блок */}
|
||||||
|
<LlmBlock
|
||||||
|
path="/foundation/logo"
|
||||||
|
version="v1.0"
|
||||||
|
specText={LLM_LOGO_TEXT}
|
||||||
|
>
|
||||||
|
<LlmSection title="Файлы логотипа" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Файл", "Формат", "Путь", "Статус"]}
|
||||||
|
rows={[
|
||||||
|
["logo-transparent.png", "PNG с прозрачным фоном", "/public/logo/logo-transparent.png", "✓ Доступен"],
|
||||||
|
["logo.svg / logo.ai", "Вектор", "—", "Ожидается от дизайнера"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Цветовые варианты" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Вариант", "Фон", "CSS-фильтр", "Применение"]}
|
||||||
|
rows={[
|
||||||
|
["Основной", "Светлый (#f8f9fa)", "нет", "Сайт, полиграфия на белом"],
|
||||||
|
["Инвертированный", "#5b7b87 (073M)", "brightness(0) invert(1)", "Хедер, тёмные секции"],
|
||||||
|
["На форме (беж)", "#c4a882 (081M)", "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)", "Форма сотрудников (бежевый костюм)"],
|
||||||
|
["На форме (синий)", "#1b4c72 (050M)", "brightness(0) invert(1)", "Форма сотрудников (синий костюм)"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Охранная зона и размеры на форме" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Носитель", "Условие", "Длина", "Высота", "Расположение"]}
|
||||||
|
rows={[
|
||||||
|
["Форма сотрудников", "Размер до 46 р.", "70 мм", "25,5 мм", "Левая сторона груди"],
|
||||||
|
["Форма сотрудников", "Размер от 48 р.", "90 мм", "32,8 мм", "Левая сторона груди"],
|
||||||
|
["Охранная зона", "Все носители", "≥ высота буквы «К»", "≥ высота буквы «К»", "Вокруг логотипа со всех сторон"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила использования" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "Применять только одобренные цветовые варианты" },
|
||||||
|
{ ok: true, text: "Соблюдать охранную зону (≥ высота буквы «К»)" },
|
||||||
|
{ ok: true, text: "Использовать PNG с прозрачным фоном для digital" },
|
||||||
|
{ ok: true, text: "Белый логотип на тёмных фонах (073M, 050M, 080M)" },
|
||||||
|
{ ok: false, text: "Не изменять пропорции или искажать логотип" },
|
||||||
|
{ ok: false, text: "Не изменять цвета элементов логотипа" },
|
||||||
|
{ ok: false, text: "Не добавлять рядом произвольный текст" },
|
||||||
|
{ ok: false, text: "Не размещать на фоне без достаточного контраста" },
|
||||||
|
{ ok: false, text: "Не применять тени, обводки, градиенты" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
{/* 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,191 @@
|
|||||||
|
@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 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,178 @@
|
|||||||
|
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 TransportPage() {
|
||||||
|
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.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
|
||||||
|
title="Макет трамвая"
|
||||||
|
subtitle="Вид с обеих сторон: бирюзовая полоса, логотип клиники, фотографии пациентов и врачей, контактная информация."
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-xl overflow-hidden border"
|
||||||
|
style={{ borderColor: "var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/offline/transport/tram-mockup.jpeg"
|
||||||
|
alt="Макет брендирования трамвая: вид спереди и сзади с логотипом Клиники ухо горло нос им. проф. Е.Н.Оленевой"
|
||||||
|
width={1884}
|
||||||
|
height={977}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</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)" }}>
|
||||||
|
{["Зона", "Цвет", "Oracal", "Высота", "Содержимое"].map(h => (
|
||||||
|
<th
|
||||||
|
key={h}
|
||||||
|
className="text-left px-4 py-3 font-medium"
|
||||||
|
style={{ color: "var(--bb-text-muted)" }}
|
||||||
|
>
|
||||||
|
{h}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
["Верхняя полоса", "#7ecfca", "053M", "~17% высоты", "Декоративная"],
|
||||||
|
["Центральная полоса", "#c4a882", "081M", "~48% высоты", "Логотип клиники (по центру)"],
|
||||||
|
["Нижняя полоса", "#5b7b87", "073M", "~35% высоты", "Декоративная / контактная информация"],
|
||||||
|
].map(([zone, hex, oracal, height, content]) => (
|
||||||
|
<tr
|
||||||
|
key={zone}
|
||||||
|
className="border-t"
|
||||||
|
style={{ borderColor: "var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 font-medium" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{zone}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-5 h-5 rounded"
|
||||||
|
style={{ background: hex as string, border: "1px solid #e5e7eb" }}
|
||||||
|
/>
|
||||||
|
<span className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{hex}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{oracal}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3" style={{ color: "var(--bb-text-muted)" }}>{height}</td>
|
||||||
|
<td className="px-4 py-3" style={{ color: "var(--bb-text-muted)" }}>{content}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Логотип на транспорте */}
|
||||||
|
<Section
|
||||||
|
title="Размещение логотипа"
|
||||||
|
subtitle="На центральной полосе, горизонтально по центру борта."
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
{[
|
||||||
|
{ label: "Минимальная длина логотипа", value: "600 мм" },
|
||||||
|
{ label: "Отступ от краёв борта", value: "≥ 400 мм" },
|
||||||
|
{ label: "Вертикальное выравнивание", value: "По центру полосы" },
|
||||||
|
{ label: "Цвет логотипа", value: "Тёмно-коричневый (080M)" },
|
||||||
|
].map(({ label, value }) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex items-center gap-4 p-4 rounded-xl border"
|
||||||
|
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ background: "var(--brand-053m)" }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{label}</p>
|
||||||
|
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>{value}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="rounded-xl border p-4 text-sm flex items-start gap-3"
|
||||||
|
style={{ borderColor: "#fde68a", background: "#fffbeb", color: "#92400e" }}
|
||||||
|
>
|
||||||
|
<span className="text-base shrink-0">⚠️</span>
|
||||||
|
<p>
|
||||||
|
Финальные макеты для производства предоставляются дизайнером в форматах AI / PDF с
|
||||||
|
реальными размерами конкретного транспортного средства.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</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,177 @@
|
|||||||
|
"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/badges", soon: true },
|
||||||
|
{ label: "Алерты", href: "/components/alerts", soon: true },
|
||||||
|
{ label: "Модальные окна", href: "/components/modals", soon: true },
|
||||||
|
{ label: "Таблицы", href: "/components/tables", soon: true },
|
||||||
|
{ label: "Навигация", href: "/components/navigation", soon: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Блоки",
|
||||||
|
items: [
|
||||||
|
{ label: "Hero", href: "/blocks/hero" },
|
||||||
|
{ label: "CEO-текст", href: "/blocks/ceo" },
|
||||||
|
{ label: "Наши врачи", href: "/blocks/doctors", soon: true },
|
||||||
|
{ label: "Отзывы", href: "/blocks/reviews", soon: true },
|
||||||
|
{ label: "Новости", href: "/blocks/news", soon: true },
|
||||||
|
{ label: "Формы контакта", href: "/blocks/contact-forms", soon: true },
|
||||||
|
{ label: "Контакт", href: "/blocks/contact", soon: true },
|
||||||
|
{ label: "Услуги", href: "/blocks/services", soon: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: "/experiments", 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.0
|
||||||
|
</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;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 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 |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: brandbook_postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: brandbook
|
||||||
|
POSTGRES_PASSWORD: brandbook
|
||||||
|
POSTGRES_DB: brandbook
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Деплой — oclinica-brandbook
|
||||||
|
|
||||||
|
## Текущее состояние
|
||||||
|
|
||||||
|
| Сервис | Статус | URL | Платформа |
|
||||||
|
|------------|-------------|----------------------------------------|---------------|
|
||||||
|
| Фронтенд | ✅ Активен | https://web-oclinica.vercel.app | Vercel Hobby |
|
||||||
|
| Бэкенд | локально | http://localhost:3001 | Docker Compose |
|
||||||
|
| База данных | локально | localhost:5433 | PostgreSQL 16 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фронтенд — Vercel
|
||||||
|
|
||||||
|
### Первоначальная настройка (уже выполнена)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Установить Vercel CLI
|
||||||
|
npm install -g vercel
|
||||||
|
|
||||||
|
# 2. Войти в аккаунт (однократно, открывает браузер)
|
||||||
|
vercel login
|
||||||
|
|
||||||
|
# 3. Первый деплой из директории apps/web
|
||||||
|
cd apps/web
|
||||||
|
vercel --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Деплой обновлений
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/web
|
||||||
|
vercel --prod --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Деплой занимает ~30 секунд. После завершения изменения сразу доступны по адресу:
|
||||||
|
**https://web-oclinica.vercel.app**
|
||||||
|
|
||||||
|
### Как это работает
|
||||||
|
|
||||||
|
- Vercel автоматически определяет Next.js и использует pnpm для сборки
|
||||||
|
- Каждый `vercel --prod` создаёт новый immutable deployment и привязывает его к production URL
|
||||||
|
- Предыдущие деплои остаются доступны по уникальным preview URL
|
||||||
|
- Логи билда: https://vercel.com/oclinica/web
|
||||||
|
|
||||||
|
### Ограничения Vercel Hobby (бесплатный план)
|
||||||
|
|
||||||
|
| Параметр | Лимит |
|
||||||
|
|-----------------------|-------------------------|
|
||||||
|
| Bandwidth | 100 GB / месяц |
|
||||||
|
| Builds | 6000 минут / месяц |
|
||||||
|
| Serverless Functions | 100 GB-hours / месяц |
|
||||||
|
| Тип использования | Только некоммерческие |
|
||||||
|
|
||||||
|
Для брендбука (внутренний инструмент) лимитов более чем достаточно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Локальная разработка
|
||||||
|
|
||||||
|
### Запуск фронтенда
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Из корня monorepo
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Или только фронтенд
|
||||||
|
cd apps/web && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Доступен на: http://localhost:3001
|
||||||
|
|
||||||
|
### Запуск бэкенда + БД
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустить PostgreSQL
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Запустить NestJS
|
||||||
|
cd apps/api && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### .env файлы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Скопировать и заполнить
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Содержимое `.env.example`:
|
||||||
|
```
|
||||||
|
DATABASE_URL="postgresql://brandbook:brandbook@localhost:5433/brandbook"
|
||||||
|
API_PORT=3001
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Автодеплой через Gitea Actions (планируется в Sprint 12)
|
||||||
|
|
||||||
|
Для автоматического деплоя при пуше в ветку `main` создать файл
|
||||||
|
`.gitea/workflows/deploy-frontend.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy Frontend to Vercel
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'apps/web/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: apps/web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install Vercel CLI
|
||||||
|
run: npm install -g vercel
|
||||||
|
|
||||||
|
- name: Deploy to Vercel
|
||||||
|
run: cd apps/web && vercel --prod --yes --token ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Настройка:**
|
||||||
|
1. Получить Vercel Token: https://vercel.com/account/tokens
|
||||||
|
2. Добавить в Gitea: Settings → Secrets → `VERCEL_TOKEN`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Хостинг бэкенда (планируется к Sprint 11)
|
||||||
|
|
||||||
|
Бэкенд (NestJS + PostgreSQL) потребуется для экспериментальной секции (Sprint 11).
|
||||||
|
Варианты для рассмотрения:
|
||||||
|
|
||||||
|
| Платформа | PostgreSQL | Бесплатно | Карта |
|
||||||
|
|-----------|-----------|-----------|-------|
|
||||||
|
| Railway | ✅ | $5 кредит / месяц | нужна |
|
||||||
|
| Supabase | ✅ | ✅ (PostgreSQL managed) | нет |
|
||||||
|
| Fly.io | ✅ | ✅ ограниченно | нет |
|
||||||
|
| VPS клиники | ✅ | ✅ (если есть) | нет |
|
||||||
|
|
||||||
|
Рекомендация: **Supabase** для БД (бесплатно, managed PostgreSQL) + **Railway** или VPS для NestJS.
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
# LLM Context — Цифровой брендбук Клиники
|
||||||
|
|
||||||
|
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||||
|
|
||||||
|
**Версия контекста:** 4.1
|
||||||
|
**Дата обновления:** 2026-03-22
|
||||||
|
**Актуальный спринт:** Sprint 5
|
||||||
|
**Сайт клиники:** https://oclinica.ru
|
||||||
|
**Брендбук (локально):** http://localhost:3001
|
||||||
|
**Брендбук (production):** https://web-oclinica.vercel.app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Назначение файла
|
||||||
|
|
||||||
|
Этот файл — единый источник дизайн-данных клиники в машиночитаемом формате.
|
||||||
|
Используется как контекст для LLM при:
|
||||||
|
- разработке страниц и компонентов сайта
|
||||||
|
- создании макетов и прототипов
|
||||||
|
- разработке мобильных приложений
|
||||||
|
- проектировании новых носителей бренда
|
||||||
|
- генерации CSS / Tailwind / Figma Tokens
|
||||||
|
|
||||||
|
При работе с любым материалом клиники **всегда загружай этот файл первым**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. О клинике
|
||||||
|
|
||||||
|
**Полное название:** Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||||
|
**Тип:** Медицинская клиника, отоларингология (ЛОР)
|
||||||
|
**Город:** Пермь
|
||||||
|
**Платформа сайта:** Drupal (тема `clinic_bootstrap_mobile`)
|
||||||
|
**Аудитория:** Пациенты, врачи клиники, подрядчики
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Цветовая палитра
|
||||||
|
|
||||||
|
### 2.1 Фирменные цвета (Oracal — базовая палитра)
|
||||||
|
|
||||||
|
Основаны на кодах самоклеящейся плёнки Oracal. HEX-значения рассчитаны по **точным RGB
|
||||||
|
из каталога Oracal**. Для печати и оффлайн-носителей используй коды Oracal, не HEX.
|
||||||
|
|
||||||
|
| Oracal | Название | HEX | RGB | HSL | CSS-переменная | Применение |
|
||||||
|
|--------|-----------------------|-----------|-------------------|-------------------|------------------|------------|
|
||||||
|
| 053M | Основной бирюзовый | `#0089c3` | rgb(0,137,195) | hsl(198,100%,38%) | `--brand-053m` | Акцент, CTA-кнопки, иконки, активные состояния |
|
||||||
|
| 073M | Тёмный серо-голубой | `#53514e` | rgb(83,81,78) | hsl(38,3%,32%) | `--brand-073m` | Тёмный фон, хедер, заголовки навигации |
|
||||||
|
| 066M | Средний бирюзовый | `#00818c` | rgb(0,129,140) | hsl(185,100%,27%) | `--brand-066m` | Вторичные акценты, фоны секций, иллюстрации |
|
||||||
|
| 050M | Тёмно-синий | `#1b2e5d` | rgb(27,46,93) | hsl(223,55%,24%) | `--brand-050m` | Наружная реклама, полиграфия, заголовки на светлом |
|
||||||
|
| 081M | Бежевый | `#a8885c` | rgb(168,136,92) | hsl(35,30%,51%) | `--brand-081m` | Форма сотрудников, оффлайн носители, тёплые акценты |
|
||||||
|
| 080M | Тёмно-коричневый | `#432f1e` | rgb(67,47,30) | hsl(27,38%,19%) | `--brand-080m` | Текст на бежевых поверхностях, логотип на форме |
|
||||||
|
| — | Белый | `#ffffff` | rgb(255,255,255) | hsl(0,0%,100%) | `--brand-white` | Фон, инвертированный текст, логотип на тёмных фонах |
|
||||||
|
|
||||||
|
### 2.2 Цвета сайта oclinica.ru (реальный CSS)
|
||||||
|
|
||||||
|
Извлечены из CSS темы Drupal:
|
||||||
|
`https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css`
|
||||||
|
Метод: python + regex + Counter, 2026-03-22
|
||||||
|
|
||||||
|
| Название | HEX | RGB | Группа | Вхождений | Применение на сайте |
|
||||||
|
|-----------------------|-----------|-------------------|----------|-----------|---------------------|
|
||||||
|
| Бежевый | `#bf9975` | rgb(191,153,117) | Акценты | 12 | Основной тёплый акцент, фоны, рамки, текст |
|
||||||
|
| Серо-бирюзовый | `#60959c` | rgb(96,149,156) | Акценты | 7 | Основной холодный акцент, ссылки |
|
||||||
|
| Бирюзовый | `#63bac3` | rgb(99,186,195) | Акценты | 4 | Фоны акцентных блоков, иконки |
|
||||||
|
| Бирюзовый средний | `#52b4bd` | rgb(82,180,189) | Акценты | 4 | Вторичные цветовые акценты |
|
||||||
|
| Коралловый | `#ffa39c` | rgb(255,163,156) | Акценты | 2 | CTA-кнопки («Запишите меня!») |
|
||||||
|
| Основной текст | `#464646` | rgb(70,70,70) | Текст | 3 | Цвет основного текста на сайте |
|
||||||
|
| Второстепенный текст | `#949290` | rgb(148,146,144) | Текст | 4 | Подписи, второстепенный контент |
|
||||||
|
| Светло-бирюзовый фон | `#b8e6ed` | rgb(184,230,237) | Фоны | 1 | Фоны светлых секций с акцентом |
|
||||||
|
| Кремовый фон | `#e9e4d4` | rgb(233,228,212) | Фоны | 1 | Тёплые фоны секций |
|
||||||
|
| Светло-жёлтый фон | `#eef4d1` | rgb(238,244,209) | Фоны | 1 | Фон карточек отзывов |
|
||||||
|
| Светло-зелёный фон | `#f2fee6` | rgb(242,254,230) | Фоны | 1 | Фон секции новостей |
|
||||||
|
|
||||||
|
### 2.3 Соответствие Oracal → Сайт
|
||||||
|
|
||||||
|
| Oracal (бренд) | HEX бренда | Сайт (цифровой) | HEX сайта | Отклонение |
|
||||||
|
|--------------------------|------------|-----------------------|-----------|------------|
|
||||||
|
| 053M Основной бирюзовый | `#0089c3` | Бирюзовый | `#63bac3` | Oracal ярко-синий; сайт светлее и голубее |
|
||||||
|
| 073M Тёмный серо-голубой | `#53514e` | Серо-бирюзовый | `#60959c` | Oracal тёмно-серый; сайт серо-бирюзовый (значительное расхождение) |
|
||||||
|
| 066M Средний бирюзовый | `#00818c` | Бирюзовый средний | `#52b4bd` | Oracal тёмный бирюзовый; сайт светлее |
|
||||||
|
| 081M Бежевый | `#a8885c` | Бежевый | `#bf9975` | Oracal тёплый охристый; сайт прохладнее и светлее |
|
||||||
|
| 050M Тёмно-синий | `#1b2e5d` | — | — | Не найден в CSS сайта |
|
||||||
|
| 080M Тёмно-коричневый | `#432f1e` | — | — | Не найден в CSS сайта |
|
||||||
|
|
||||||
|
**Важно:** расхождения ожидаемы — это цифровая адаптация плёночных цветов под экран.
|
||||||
|
При разработке digital-материалов используй цвета сайта (раздел 2.2), не Oracal.
|
||||||
|
|
||||||
|
### 2.4 Контрастность пар (WCAG 2.1)
|
||||||
|
|
||||||
|
| Пара | Коэффициент | AA (4.5:1) | AAA (7:1) | AA large (3:1) |
|
||||||
|
|----------------------------------------------|-------------|------------|-----------|----------------|
|
||||||
|
| Белый на тёмном серо-голубом (#53514e 073M) | 7.9:1 | ✓ | ✓ | ✓ |
|
||||||
|
| Белый на тёмно-синем (#1b2e5d 050M) | 13.2:1 | ✓ | ✓ | ✓ |
|
||||||
|
| Белый на тёмно-коричневом (#432f1e 080M) | 12.6:1 | ✓ | ✓ | ✓ |
|
||||||
|
| Белый на среднем бирюзовом (#00818c 066M) | 4.7:1 | ✓ | ✕ | ✓ |
|
||||||
|
| Тёмный (#111827) на основном бирюзовом (#0089c3 053M) | 4.7:1 | ✓ | ✕ | ✓ |
|
||||||
|
| Тёмный (#111827) на бежевом (#a8885c 081M) | 5.5:1 | ✓ | ✕ | ✓ |
|
||||||
|
| Тёмно-коричневый (#432f1e) на бежевом (#a8885c) | 3.8:1 | ✕ | ✕ | ✓ (только крупный ≥18pt) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Типографика
|
||||||
|
|
||||||
|
### 3.1 DINPro — фирменный шрифт (оффлайн)
|
||||||
|
|
||||||
|
**Применение:** логотип, оффлайн-носители (бейджи, навигационные таблички, транспорт,
|
||||||
|
форма сотрудников, полиграфия).
|
||||||
|
**Не использовать** на веб-сайте и в digital-материалах (лицензионный шрифт,
|
||||||
|
нет легального встраивания в веб).
|
||||||
|
**Веса:** Regular, Medium, Bold
|
||||||
|
|
||||||
|
| Стиль | font-weight | font-size | line-height | letter-spacing | Применение |
|
||||||
|
|----------|-------------|-----------|-------------|----------------|------------|
|
||||||
|
| Display | 700 | 48px | 1.15 | -0.5px | Крупные заголовки носителей |
|
||||||
|
| H1 | 700 | 36px | 1.2 | -0.3px | Основной заголовок печатных материалов |
|
||||||
|
| H2 | 700 | 28px | 1.25 | -0.2px | Подзаголовки носителей |
|
||||||
|
| H3 | 500 | 22px | 1.3 | 0px | Подзаголовки третьего уровня |
|
||||||
|
| Body | 400 | 16px | 1.5 | 0px | Основной текст оффлайн |
|
||||||
|
| Small | 400 | 12px | 1.4 | 0.2px | Подписи, бейджи, таблички |
|
||||||
|
| Caption | 400 | 10px | 1.3 | 0.4px | Сноски, технические пометки |
|
||||||
|
|
||||||
|
### 3.2 Fira Sans — веб-шрифт (digital)
|
||||||
|
|
||||||
|
**Применение:** сайт oclinica.ru, цифровые коммуникации, digital-баннеры, email.
|
||||||
|
**Источник:** Google Fonts (`https://fonts.google.com/specimen/Fira+Sans`)
|
||||||
|
**Поддержка кириллицы:** да
|
||||||
|
**Используемые веса:** 300 (Light), 400 (Regular), 500 (Medium), 600 (SemiBold)
|
||||||
|
**CSS:** `font-family: 'Fira Sans', sans-serif;`
|
||||||
|
|
||||||
|
| Стиль | font-weight | font-size | line-height | letter-spacing | Применение |
|
||||||
|
|----------------|-------------|---------------|-------------|----------------|------------|
|
||||||
|
| h1 | 600 | 2.25rem (36px)| 1.25 | -0.025em | Заголовок страницы |
|
||||||
|
| h2 | 600 | 1.875rem (30px)| 1.3 | -0.02em | Заголовок секции |
|
||||||
|
| h3 | 500 | 1.5rem (24px) | 1.375 | -0.01em | Подзаголовок |
|
||||||
|
| h4 | 500 | 1.25rem (20px)| 1.4 | 0em | Заголовок компонента |
|
||||||
|
| h5 | 500 | 1.125rem (18px)| 1.45 | 0em | Подзаголовок компонента |
|
||||||
|
| h6 | 500 | 1rem (16px) | 1.5 | 0.01em | Метка секции |
|
||||||
|
| body-large | 400 | 1.125rem (18px)| 1.6 | 0em | Лид-текст, вводный абзац |
|
||||||
|
| body | 400 | 1rem (16px) | 1.625 | 0em | Основной текст |
|
||||||
|
| body-small | 400 | 0.875rem (14px)| 1.5 | 0em | Дополнительный текст, подписи |
|
||||||
|
| caption | 300 | 0.75rem (12px)| 1.4 | 0.02em | Подписи к изображениям, сноски |
|
||||||
|
| label | 500 | 0.875rem (14px)| 1.2 | 0.03em | Метки форм |
|
||||||
|
| overline | 600 | 0.6875rem (11px)| 1.2 | 0.1em | Надписи над заголовками (uppercase) |
|
||||||
|
|
||||||
|
**Принцип выбора шрифта:**
|
||||||
|
- Носитель цифровой → Fira Sans
|
||||||
|
- Носитель печатный / физический → DINPro
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Логотип
|
||||||
|
|
||||||
|
### 4.1 Версии логотипа
|
||||||
|
|
||||||
|
**Основной логотип** — включает название специализации («ухо, горло, нос»).
|
||||||
|
Используется на всех основных носителях: сайт, навигация, транспорт, форма.
|
||||||
|
|
||||||
|
**Общий логотип** — без специализации, только название клиники или сеть.
|
||||||
|
Используется для обозначения сети клиник, в корпоративных материалах.
|
||||||
|
|
||||||
|
### 4.2 Файлы
|
||||||
|
|
||||||
|
| Файл | Описание | Путь в проекте |
|
||||||
|
|------|----------|----------------|
|
||||||
|
| `logo-transparent.png` | Логотип с прозрачным фоном | `apps/web/public/logo/logo-transparent.png` |
|
||||||
|
|
||||||
|
SVG-версия ожидается (не получена от клиники).
|
||||||
|
|
||||||
|
### 4.3 Цветовые варианты
|
||||||
|
|
||||||
|
| Вариант | Фон | Логотип | Применение |
|
||||||
|
|---------|-----|---------|------------|
|
||||||
|
| Основной | Белый / светлый | Полноцветный | Сайт, полиграфия на белом |
|
||||||
|
| Инвертированный | Тёмный (#53514e, #1b2e5d) | Белый (`filter: brightness(0) invert(1)`) | Хедер, тёмные секции |
|
||||||
|
| На форме (беж) | Бежевый (#a8885c / Oracal 081M) | Коричневый (#432f1e / Oracal 080M) | Одежда персонала |
|
||||||
|
| На форме (синий) | Тёмно-синий (#1b2e5d / Oracal 050M) | Белый | Одежда персонала |
|
||||||
|
|
||||||
|
### 4.4 Охранная зона
|
||||||
|
|
||||||
|
Минимальный отступ вокруг логотипа = **высота буквы «К»** в названии.
|
||||||
|
Запрещено размещать другие элементы в охранной зоне.
|
||||||
|
|
||||||
|
### 4.5 Минимальные размеры
|
||||||
|
|
||||||
|
| Носитель | Размер логотипа |
|
||||||
|
|----------|----------------|
|
||||||
|
| Одежда до р.46 | 70 × 25,5 мм |
|
||||||
|
| Одежда от р.48 | 90 × 32,8 мм |
|
||||||
|
|
||||||
|
### 4.6 Запрещено
|
||||||
|
|
||||||
|
- Искажать пропорции
|
||||||
|
- Менять цвета на нефирменные
|
||||||
|
- Добавлять тени, обводки, эффекты
|
||||||
|
- Размещать на пёстром или плохо контрастном фоне
|
||||||
|
- Использовать низкое разрешение (< 150 dpi для печати)
|
||||||
|
- Переворачивать или отражать
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. CSS-переменные брендбука
|
||||||
|
|
||||||
|
Определены в `apps/web/app/globals.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Цвета бренда (точные RGB из каталога Oracal) */
|
||||||
|
--brand-053m: #0089c3; /* Основной бирюзовый · rgb(0,137,195) */
|
||||||
|
--brand-073m: #53514e; /* Тёмный серо-голубой · rgb(83,81,78) */
|
||||||
|
--brand-066m: #00818c; /* Средний бирюзовый · rgb(0,129,140) */
|
||||||
|
--brand-050m: #1b2e5d; /* Тёмно-синий · rgb(27,46,93) */
|
||||||
|
--brand-081m: #a8885c; /* Бежевый · rgb(168,136,92) */
|
||||||
|
--brand-080m: #432f1e; /* Тёмно-коричневый · rgb(67,47,30) */
|
||||||
|
--brand-white: #ffffff; /* Белый */
|
||||||
|
|
||||||
|
/* UI брендбука */
|
||||||
|
--bb-sidebar-bg: #f8f9fa;
|
||||||
|
--bb-sidebar-border: #e5e7eb;
|
||||||
|
--bb-sidebar-text: #374151;
|
||||||
|
--bb-sidebar-text-muted: #6b7280;
|
||||||
|
--bb-sidebar-active-bg: #dff0fa; /* светло-синий под 053M */
|
||||||
|
--bb-sidebar-active-text: var(--brand-053m);
|
||||||
|
--bb-sidebar-section: #9ca3af;
|
||||||
|
--bb-content-bg: #ffffff;
|
||||||
|
--bb-border: #e5e7eb;
|
||||||
|
--bb-text: #111827;
|
||||||
|
--bb-text-muted: #6b7280;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Оффлайн носители
|
||||||
|
|
||||||
|
### 6.1 Форма сотрудников
|
||||||
|
|
||||||
|
**Варианты:**
|
||||||
|
- Бежевый: ткань цвета Oracal 081M (#a8885c), логотип Oracal 080M (#432f1e, коричневый), расположение — левая сторона груди
|
||||||
|
- Синий: ткань цвета Oracal 050M (#1b2e5d, тёмно-синий), логотип белый (Oracal 010), расположение — левая сторона груди
|
||||||
|
|
||||||
|
**Размеры логотипа на форме:**
|
||||||
|
- Размеры до 46: 70 × 25,5 мм
|
||||||
|
- Размеры от 48: 90 × 32,8 мм
|
||||||
|
|
||||||
|
### 6.2 Бейджи
|
||||||
|
|
||||||
|
**Размер:** 70 × 30 мм
|
||||||
|
**Варианты:** серый (светлый) и белый
|
||||||
|
**Состав текста:** имя, должность, учёная степень (при наличии)
|
||||||
|
**Шрифт:** DINPro
|
||||||
|
**Логотип:** левый верхний угол или левая часть
|
||||||
|
|
||||||
|
### 6.3 Внутренняя навигация
|
||||||
|
|
||||||
|
**Материал:** оргстекло
|
||||||
|
**Плёнки:** Oracal 053M (#0089c3) и 073M (#53514e)
|
||||||
|
**Типы табличек:**
|
||||||
|
- Таблички на дверях кабинетов: номер кабинета, профиль врача с фото и QR-кодом
|
||||||
|
- Указатели по этажам: стрелки направлений + номера кабинетов
|
||||||
|
|
||||||
|
**Нумерация кабинетов:**
|
||||||
|
- Двузначное число, крупный шрифт (DINPro Bold)
|
||||||
|
- Фон: бирюзовый (053M), номер: белый
|
||||||
|
- Пример реализации: кабинеты 13, 31, «Кабинет 04»
|
||||||
|
|
||||||
|
### 6.4 Брендирование транспорта (трамвай)
|
||||||
|
|
||||||
|
**Зоны оклейки:**
|
||||||
|
- Борта: Oracal 053M (#0089c3) + 073M (#53514e)
|
||||||
|
- Передняя часть: Oracal 066M (#00818c) + 050M (#1b2e5d)
|
||||||
|
- Акценты: Oracal 081M (#a8885c) + 080M (#432f1e)
|
||||||
|
|
||||||
|
**Все 6 фирменных цветов присутствуют на транспорте.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Структура брендбука (страницы)
|
||||||
|
|
||||||
|
| URL | Статус | Описание |
|
||||||
|
|-----|--------|----------|
|
||||||
|
| `/foundation/logo` | ✅ Готова | Логотип, варианты, охранная зона, правила |
|
||||||
|
| `/foundation/colors` | ✅ Готова | Палитра (Oracal точные RGB), контраст WCAG, цвета сайта, соответствие |
|
||||||
|
| `/foundation/typography` | ✅ Готова | DINPro + Fira Sans, шкала стилей |
|
||||||
|
| `/foundation/icons` | 🔜 Скоро | Иконография |
|
||||||
|
| `/offline/uniform` | ✅ Готова | Форма сотрудников |
|
||||||
|
| `/offline/badges` | ✅ Готова | Бейджи |
|
||||||
|
| `/offline/navigation` | ✅ Готова | Внутренняя навигация |
|
||||||
|
| `/offline/transport` | ✅ Готова | Брендирование транспорта |
|
||||||
|
| `/components/buttons` | ✅ Готова | Кнопки — 4 варианта с реального сайта, размеры, состояния |
|
||||||
|
| `/components/forms` | ✅ Готова | Форм-контролы — Input/Textarea/Select/Checkbox/Radio/Toggle |
|
||||||
|
| `/components/cards` | 🚧 Sprint 4 | Карточки — врач, услуга, новость, отзыв, цена + бейджи/теги/алерты |
|
||||||
|
| `/components/*` | 🔜 Sprint 4–5 | Модалки, таблицы, навигация |
|
||||||
|
| `/blocks/*` | 🔜 Sprint 5–8 | Hero, врачи, отзывы, новости, формы |
|
||||||
|
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Правила применения цветов
|
||||||
|
|
||||||
|
### Разрешено
|
||||||
|
- Использовать только цвета из фирменной палитры (раздел 2.1)
|
||||||
|
- Для digital: адаптировать к цветам сайта (раздел 2.2)
|
||||||
|
- Текст на цветном фоне — минимум WCAG AA (4.5:1)
|
||||||
|
- Белый текст на тёмных фонах: 073M (#53514e), 066M (#00818c), 050M (#1b2e5d), 080M (#432f1e)
|
||||||
|
- Тёмный текст (#111827) на светлых: 053M (#0089c3), 081M (#a8885c)
|
||||||
|
- Коричневый логотип (080M) на бежевом фоне (081M) — только крупный текст (3.8:1)
|
||||||
|
|
||||||
|
### Запрещено
|
||||||
|
- Использовать произвольные цвета вне палитры
|
||||||
|
- Осветлять, затемнять или менять оттенок фирменных цветов
|
||||||
|
- Текст с контрастом ниже 3:1 (даже для крупного текста)
|
||||||
|
- Размещать тёплые и холодные акценты рядом без разделителя
|
||||||
|
|
||||||
|
### Иерархия цветов
|
||||||
|
|
||||||
|
```
|
||||||
|
Основной бирюзовый (053M / #0089c3) ← главный идентификатор бренда
|
||||||
|
└── Средний бирюзовый (066M / #00818c) ← вторичный акцент
|
||||||
|
└── Тёмный серо-голубой (073M / #53514e) ← фоны, хедер
|
||||||
|
|
||||||
|
Тёмно-синий (050M / #1b2e5d) ← авторитет, полиграфия
|
||||||
|
Бежевый (081M / #a8885c) ← тепло, форма
|
||||||
|
└── Тёмно-коричневый (080M / #432f1e) ← текст на бежевом
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Правила применения типографики
|
||||||
|
|
||||||
|
### Веб (Fira Sans)
|
||||||
|
- H1 только один на странице
|
||||||
|
- Заголовки не пропускать по уровням (h1→h2→h3)
|
||||||
|
- Основной текст: 16px / 400 / 1.625
|
||||||
|
- Минимальный размер текста на экране: 12px
|
||||||
|
- Кириллица: только Fira Sans, не DINPro
|
||||||
|
|
||||||
|
### Оффлайн (DINPro)
|
||||||
|
- Все физические носители: DINPro
|
||||||
|
- Логотип: DINPro Bold
|
||||||
|
- Таблички: DINPro Medium / Bold
|
||||||
|
- Не использовать light (300) для текста < 14pt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9a. Базовые компоненты (Sprint 3)
|
||||||
|
|
||||||
|
### Кнопки (Button) · `/components/buttons`
|
||||||
|
|
||||||
|
Варианты скопированы с реального сайта perm.oclinica.ru.
|
||||||
|
CSS-классы в `globals.css`. Компонент: `@/components/ui/Button` (React, "use client").
|
||||||
|
|
||||||
|
| Вариант | CSS класс | Фон | Текст | Граница | Border-radius | Применение |
|
||||||
|
|---------|------------------|----------|---------|-----------|---------------|------------|
|
||||||
|
| primary | .bb-btn-primary | #FFA39C | #fff | #FF847B | 7px, bold, shadow | Форма записи «Запишите меня!» |
|
||||||
|
| outline | .bb-btn-outline | #fff | #BF9975 | #BF9975 | 7px | «Записаться на приём» |
|
||||||
|
| teal | .bb-btn-teal | #60959C | #fff | прозрачная| 7px | «Позвонить» |
|
||||||
|
| pill | .bb-btn-pill | #E9E4D4 | #333 | #D5CFBD | 25px | «Заказать звонок» |
|
||||||
|
|
||||||
|
| Размер | CSS класс | padding | font-size |
|
||||||
|
|--------|------------|--------------|-----------|
|
||||||
|
| sm | .bb-btn-sm | 4px 11px | 13px |
|
||||||
|
| md | .bb-btn-md | 8px 16px | 14px |
|
||||||
|
| lg | .bb-btn-lg | 10px 24px | 18px |
|
||||||
|
|
||||||
|
**Состояния:** default · hover (brightness 0.9) · active (brightness 0.82) · loading (spinner) · disabled (opacity 0.5)
|
||||||
|
|
||||||
|
### Форм-контролы (Forms) · `/components/forms`
|
||||||
|
|
||||||
|
Размеры соответствуют entityform-блокам на реальном сайте.
|
||||||
|
|
||||||
|
| Элемент | CSS класс | Тег HTML | Высота | Описание |
|
||||||
|
|----------|------------------|----------------------------|---------|----------|
|
||||||
|
| Input | .bb-input | `<input>` | 50px | text, email, password · border 1px #ccc · radius 4px |
|
||||||
|
| Textarea | .bb-textarea | `<textarea>` | ≥100px | многострочный, resize:vertical |
|
||||||
|
| Select | .bb-select | `<select>` | 50px | с кастомной SVG-стрелкой |
|
||||||
|
| Checkbox | .bb-checkbox | `<input type="checkbox">` | 16×16px | accent-color: #53514e |
|
||||||
|
| Radio | .bb-radio | `<input type="radio">` | 16×16px | accent-color: #53514e |
|
||||||
|
| Toggle | .bb-toggle-track | React-компонент `<Toggle>` | 24px | 44×24px track + 20px thumb |
|
||||||
|
|
||||||
|
**Состояния полей:** default (border 1px solid #ccc) · focus (border #0089c3 + box-shadow rgba(0,137,195,0.2)) · error (.bb-error, border #dc2626) · disabled (opacity 0.5)
|
||||||
|
|
||||||
|
**Контекст на сайте:** фон формы #b8e6ed, ширина полей 302px, entityform-блоки.
|
||||||
|
|
||||||
|
**Toggle:** выкл → track #d1d5db · вкл → track #53514e · thumb: белый круг 20×20px.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9b. Карточки, бейджи и алерты (Sprint 4)
|
||||||
|
|
||||||
|
### Карточки · `/components/cards`
|
||||||
|
|
||||||
|
| Карточка | Размеры ключевые | Источник на сайте | Фон / hover |
|
||||||
|
|-------------|-------------------------|------------------------------------------------|--------------------------|
|
||||||
|
| DoctorCard | фото 110×160px | `.doctor .image` + `.doctor .item` | #fff / — |
|
||||||
|
| NewsCard | preview h=144px | `#block-views-last-news-block-1 .views-column` | #fff / **#eef4d1** + shadow |
|
||||||
|
| ReviewCard | 4-строчный clamp | `.node-reviews` | **#eef4d1** / — |
|
||||||
|
| PriceCard | flex row: name + price | `.field-name-field-price-priem` | #fff / highlighted: #f0f9ff |
|
||||||
|
| ServiceCard | иконка 48×48px | — (нет прямого аналога) | #fff / shadow |
|
||||||
|
|
||||||
|
**DoctorCard:** фото 110×160px (placeholder фон `#dff0fa`), кнопка `.bb-btn-outline .bb-btn-sm`
|
||||||
|
**NewsCard hover CSS:** `background: #eef4d1; box-shadow: 0 0 16px 0 #9e9e9a;` — взят с реального сайта
|
||||||
|
**ReviewCard:** звёзды SVG, заливка `#f59e0b`, `WebkitLineClamp: 4`
|
||||||
|
|
||||||
|
### Бейджи, теги, алерты
|
||||||
|
|
||||||
|
| Элемент | Варианты | Применение |
|
||||||
|
|---------|----------|------------|
|
||||||
|
| Badge | primary (#0089c3) / success (#059669) / warning (#d97706) / danger (#dc2626) / neutral (#6b7280) | Статус врача, категория, акция |
|
||||||
|
| Tag | default (border) / active (brand bg) | Фильтры, категории услуг |
|
||||||
|
| Alert | info (#dff0fa/#075985) / success (#d1fae5/#065f46) / warning (#fef3c7/#92400e) / error (#fee2e2/#991b1b) | Системные сообщения |
|
||||||
|
|
||||||
|
**CSS класс hover:** `.bb-news-card:hover` в globals.css
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Технический стек проекта
|
||||||
|
|
||||||
|
| Слой | Технология | Версия |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| Фронтенд | Next.js App Router | 16.x |
|
||||||
|
| Стилизация | Tailwind CSS | 4.x |
|
||||||
|
| Язык | TypeScript | 5.x |
|
||||||
|
| Шрифты | next/font/google | — |
|
||||||
|
| Бэкенд | NestJS | 11.x |
|
||||||
|
| БД | PostgreSQL + Prisma | 16.x / 7.x |
|
||||||
|
| Деплой | Vercel Hobby | — |
|
||||||
|
| Пакетный менеджер | pnpm | 10.x |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. История изменений контекста
|
||||||
|
|
||||||
|
| Версия | Дата | Что добавлено |
|
||||||
|
|--------|------|---------------|
|
||||||
|
| 1.0 | 2026-03-22 | Sprint 1: логотип, базовые цвета |
|
||||||
|
| 2.0 | 2026-03-22 | Sprint 2: типографика, оффлайн носители, цвета сайта (8 цветов) |
|
||||||
|
| 2.1 | 2026-03-22 | Sprint 2 доп.: +3 цвета сайта (коралловый, светло-жёлтый, светло-зелёный) |
|
||||||
|
| 3.0 | 2026-03-22 | Sprint 3: кнопки (Button), форм-контролы (Input/Textarea/Select/Checkbox/Radio/Toggle), LLM-блок на логотипе |
|
||||||
|
| 4.0 | 2026-03-22 | Sprint 4 start: исправлены цвета Oracal (точные RGB из каталога), кнопки/формы по реальному сайту |
|
||||||
|
| 4.1 | 2026-03-22 | Sprint 4 done: карточки (DoctorCard/NewsCard/ReviewCard/PriceCard/ServiceCard), бейджи/теги/алерты |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Что обновлять в этом файле
|
||||||
|
|
||||||
|
При каждом спринте добавляй:
|
||||||
|
- Новые компоненты и их спецификации (цвета, размеры, состояния)
|
||||||
|
- Новые правила применения, выявленные при разработке
|
||||||
|
- Изменения в палитре или типографике
|
||||||
|
- Новые паттерны и примеры кода
|
||||||
|
|
||||||
|
**Соответствующий раздел в TZ.md:** ФТ-03-LLM
|
||||||
|
**Соответствующий раздел в SPRINTS.md:** задача «LLM-блок» в каждом спринте
|
||||||
@@ -14,98 +14,160 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 1 — Инициализация проекта + страница «Логотип»
|
## Sprint 1 — Инициализация проекта + страница «Логотип» ✅ ЗАВЕРШЁН
|
||||||
|
|
||||||
**Цель:** Рабочее окружение, monorepo, базовая архитектура, первая живая страница брендбука — «Логотип».
|
**Цель:** Рабочее окружение, monorepo, базовая архитектура, первая живая страница брендбука — «Логотип».
|
||||||
|
|
||||||
### Задачи — инфраструктура
|
### Задачи — инфраструктура
|
||||||
- [ ] FE: Инициализация Next.js (App Router) в `apps/web`
|
- [x] FE: Инициализация Next.js 16 (App Router, Tailwind 4, TypeScript) в `apps/web`
|
||||||
- [ ] BE: Инициализация NestJS в `apps/api`
|
- [x] BE: Инициализация NestJS 11 в `apps/api`
|
||||||
- [ ] DB: Подключение PostgreSQL + Prisma, базовая схема
|
- [x] DB: PostgreSQL 16 + Prisma 7, схема User + ExperimentalComponent
|
||||||
- [ ] Настройка Docker Compose для локальной разработки
|
- [x] Настройка Docker Compose (порт 5433 — 5432 занят на хосте)
|
||||||
- [ ] Настройка monorepo (pnpm workspaces)
|
- [x] Настройка monorepo (pnpm workspaces)
|
||||||
- [ ] Создание `.env.example`
|
- [x] Создание `.env.example`
|
||||||
- [ ] Git: создание веток `develop` и `sprint/1`
|
- [x] Git: создание веток `develop` и `sprint/1`, подключён remote git.pirogov.ai
|
||||||
- [ ] FE: Базовая структура брендбука (layout, боковая навигация по разделам)
|
- [x] FE: Layout с боковой навигацией (все разделы, статус «скоро»)
|
||||||
- [ ] FE: Подключение шрифта Fira Sans (веб) + подготовка к DINPro (бренд)
|
- [x] FE: Fira Sans подключён через next/font/google, CSS-токены бренда в globals.css
|
||||||
|
|
||||||
### Задачи — страница «Логотип» (первый контент брендбука)
|
### Задачи — страница «Логотип»
|
||||||
- [ ] Design: Экспорт PNG логотипа из PDF для использования как placeholder
|
- [x] Design: PNG логотипа извлечён из PDF программно (PyMuPDF + Pillow)
|
||||||
- [ ] FE: Страница `/foundation/logo` в брендбуке
|
- [x] FE: Прозрачная версия логотипа (удалён белый фон через numpy)
|
||||||
- [ ] FE: Отображение обеих версий логотипа: «Общий» и «Основной»
|
- [x] FE: Страница `/foundation/logo`
|
||||||
- [ ] FE: Секция «Иерархия»: описание применения каждой версии
|
- [x] FE: Иерархия версий (Основной / Общий) с реальным изображением из PDF
|
||||||
- [ ] FE: Секция «Цветовые варианты»: основной / инвертированный / на форме
|
- [x] FE: Цветовые варианты: светлый / инвертированный (CSS filter) / на форме
|
||||||
- [ ] FE: Секция «Охранная зона» с визуализацией отступов
|
- [x] FE: Охранная зона с визуализацией отступов
|
||||||
- [ ] FE: Секция «Минимальные размеры» (таблица: до 46 р. и от 48 р.)
|
- [x] FE: Таблица минимальных размеров (до 46 р. и от 48 р.)
|
||||||
- [ ] FE: Секция «Недопустимые варианты» — правило не менять и не сочетать с текстом
|
- [x] FE: 6 правил недопустимого использования
|
||||||
- [ ] FE: Placeholder-блок «Скачать вектор» (кнопка неактивна до получения SVG)
|
- [x] FE: Placeholder «Скачать вектор» (кнопка неактивна)
|
||||||
|
|
||||||
**Результат спринта:** Запускается `pnpm dev`, открывается брендбук с навигацией. Раздел «Логотип» полностью заполнен контентом и правилами.
|
### Фактические результаты
|
||||||
|
- Брендбук запущен локально на `http://localhost:3001`
|
||||||
|
- Название клиники исправлено: «Клиника ухо, горло, нос им. проф. Е.Н.Оленевой»
|
||||||
|
- Инвертированный логотип корректно отображается (белый на тёмном фоне)
|
||||||
|
|
||||||
|
### Технические решения Sprint 1
|
||||||
|
- PostgreSQL запущен на порту **5433** (5432 занят на хосте)
|
||||||
|
- Логотип хранится как PNG с прозрачным фоном (`public/logo/logo-transparent.png`)
|
||||||
|
- Инверсия логотипа: CSS `filter: brightness(0) invert(1)` на прозрачном PNG
|
||||||
|
- Next.js запускается на порту **3001** (3000 занят на хосте)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 2 — Цвета, типографика и оффлайн элементы (Brand Foundation)
|
## Sprint 2 — Цвета, типографика и оффлайн элементы (Brand Foundation) ✅ ЗАВЕРШЁН
|
||||||
|
|
||||||
**Цель:** Секция «Фундамент бренда» — цвета, оба шрифта, а также первый оффлайн-раздел.
|
**Цель:** Секция «Фундамент бренда» — цвета, оба шрифта, а также первый оффлайн-раздел.
|
||||||
|
|
||||||
### Задачи — цвета
|
### Задачи — цвета
|
||||||
- [ ] Design: Зафиксировать HEX-эквиваленты всех цветов бренда (053M, 073M, 066M, 050M, 081M, 080M)
|
- [x] Design: Зафиксировать HEX-эквиваленты всех цветов бренда (053M, 073M, 066M, 050M, 081M, 080M)
|
||||||
- [ ] FE: Создать CSS-переменные / дизайн-токены для всей цветовой палитры
|
- [x] FE: Создать CSS-переменные / дизайн-токены для всей цветовой палитры
|
||||||
- [ ] FE: Страница «Цвета» — палитра с кодами Oracal/Pantone, HEX, RGB, HSL, копирование в клик
|
- [x] FE: Страница «Цвета» — палитра с кодами Oracal/Pantone, HEX, RGB, HSL, копирование в клик
|
||||||
- [ ] FE: Проверка контрастности пар цветов (WCAG AA/AAA)
|
- [x] FE: Проверка контрастности пар цветов (WCAG AA/AAA)
|
||||||
- [ ] FE: Экспорт токенов в JSON (Figma-compatible)
|
- [x] FE: Экспорт токенов в JSON (Figma-compatible)
|
||||||
|
|
||||||
### Задачи — типографика
|
### Задачи — типографика
|
||||||
- [ ] FE: Страница «Типографика» — раздел DINPro (бренд/оффлайн) + раздел Fira Sans (веб)
|
- [x] FE: Страница «Типографика» — раздел DINPro (бренд/оффлайн) + раздел Fira Sans (веб)
|
||||||
- [ ] FE: Шкала размеров для обоих шрифтов: h1–h6, body, caption, label, overline
|
- [x] FE: Шкала размеров для обоих шрифтов: h1–h6, body, caption, label, overline
|
||||||
- [ ] FE: Чёткое указание: где DINPro, где Fira Sans
|
- [x] FE: Чёткое указание: где DINPro, где Fira Sans
|
||||||
- [ ] FE: Живые примеры текста обоими шрифтами
|
- [x] FE: Живые примеры текста обоими шрифтами
|
||||||
|
|
||||||
### Задачи — оффлайн элементы (справочный раздел)
|
### Задачи — оффлайн элементы (справочный раздел)
|
||||||
- [ ] FE: Раздел «Оффлайн элементы» в навигации брендбука
|
- [x] FE: Раздел «Оффлайн элементы» в навигации брендбука
|
||||||
- [ ] FE: Страница «Форма сотрудников» — фото, правила размещения логотипа, таблица размеров
|
- [x] FE: Страница «Форма сотрудников» — схема размещения логотипа, таблица размеров
|
||||||
- [ ] FE: Страница «Бейджи» — размеры 70×30 мм, варианты, состав текста
|
- [x] FE: Страница «Бейджи» — размеры 70×30 мм, варианты (светлый/тёмный), состав текста
|
||||||
- [ ] FE: Страница «Внутренняя навигация» — шаблоны табличек, материалы, цвета Oracal
|
- [x] FE: Страница «Внутренняя навигация» — шаблоны табличек, материалы, цвета Oracal
|
||||||
- [ ] FE: Страница «Брендирование транспорта» — макет трамвая, цветовая схема
|
- [x] FE: Страница «Брендирование транспорта» — макет трамвая, цветовая схема
|
||||||
- [ ] FE: Страница «Печатные материалы» — листовки, визитки, Telegram-бот
|
- [x] FE: Страница «Печатные материалы» — листовки, визитки, Telegram-бот
|
||||||
|
|
||||||
**Результат спринта:** Разделы «Цвета», «Типографика» и «Оффлайн элементы» полностью готовы.
|
### Фактические результаты
|
||||||
|
- Страница `/foundation/colors` — 7 цветов с HEX/RGB/HSL/CSS-var и копированием, WCAG-контраст 7 пар, экспорт JSON
|
||||||
|
- Страница `/foundation/typography` — DINPro (оффлайн) + Fira Sans (веб), таблица применения, полные шкалы, живой пример
|
||||||
|
- Страница `/offline/uniform` — реальные фото из PDF (беж + синий вариант), таблица размеров, правила
|
||||||
|
- Страница `/offline/badges` — реальные фото из PDF (лицевая + оборотная), состав текста, пример
|
||||||
|
- Страница `/offline/navigation` — макеты из PDF (Кабинет 04, карточка врача), фото дверей с номерами (13, 31), указатели по этажам
|
||||||
|
- Страница `/offline/transport` — макет трамвая из PDF (оба вида, реальный рендер), таблица зон, цвета Oracal
|
||||||
|
- Страница `/offline/print` — убрана из навигации (нет данных из брендбука)
|
||||||
|
- Sidebar: убраны «скоро» для Цветов, Типографики и всех страниц Оффлайн кроме Печати
|
||||||
|
- Версия обновлена до **Sprint 2 · v0.2.0**
|
||||||
|
- **Деплой на Vercel:** https://web-oclinica.vercel.app (production, бесплатно)
|
||||||
|
- **Тайтлы страниц:** единый формат «Раздел. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой»
|
||||||
|
|
||||||
|
### Технические решения Sprint 2
|
||||||
|
- Страница «Цвета» — `"use client"` для clipboard API и экспорта JSON
|
||||||
|
- WCAG relative luminance вычисляется на клиенте, без зависимостей
|
||||||
|
- DINPro отображается с фоллбэком Arial (лицензионный шрифт)
|
||||||
|
- Реальные фото и макеты из PDF: PyMuPDF (fitz) — извлечение растровых изображений и рендер векторных страниц
|
||||||
|
- Рендер PDF страниц: 2.5–3.0x масштаб → JPEG, кроп до нужной области через Pillow
|
||||||
|
|
||||||
|
### Ретроспектива Sprint 2 — LLM-контекст
|
||||||
|
- [x] Docs: Создан `docs/LLM_CONTEXT.md` — сводный файл дизайн-данных для LLM (v2.1)
|
||||||
|
- [x] TZ: Добавлено требование ФТ-03-LLM — LLM-блок на каждой странице брендбука
|
||||||
|
- [ ] FE: Добавить LLM-блоки на страницы Sprint 1–2 (логотип, цвета, типографика, оффлайн) — перенесено в Sprint 3
|
||||||
|
|
||||||
|
**Результат спринта:** Разделы «Цвета», «Типографика» и «Оффлайн элементы» полностью готовы с реальными материалами из брендбука.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 3 — Базовые компоненты: кнопки и поля ввода
|
## Sprint 3 — Базовые компоненты: кнопки и поля ввода ✅ ЗАВЕРШЁН
|
||||||
|
|
||||||
**Цель:** Все варианты кнопок и форм-контролов в брендбуке.
|
**Цель:** Все варианты кнопок и форм-контролов в брендбуке. LLM-блоки на страницах.
|
||||||
|
|
||||||
### Задачи
|
### Задачи — LLM-контекст
|
||||||
- [ ] FE: Компонент Button (все варианты: primary/secondary/ghost/danger, размеры, состояния)
|
- [x] FE: Добавить LLM-блок на страницу «Логотип» (`/foundation/logo`) — v1.0
|
||||||
- [ ] FE: Компонент Input (text, password, focus/error/disabled)
|
- [x] FE: Добавить LLM-блок на страницу «Цвета» (`/foundation/colors`) — v2.1
|
||||||
- [ ] FE: Компонент Textarea
|
- [x] FE: Добавить LLM-блок на страницу «Типографика» (`/foundation/typography`) — v2.0
|
||||||
- [ ] FE: Компонент Select
|
- [x] FE: Создать переиспользуемый компонент `components/llm/LlmBlock.tsx` (LlmBlock, LlmSection, LlmTable, LlmRules)
|
||||||
- [ ] FE: Компонент Checkbox и Radio
|
- [x] Docs: Обновить `docs/LLM_CONTEXT.md` по итогам спринта — версия 3.0
|
||||||
- [ ] FE: Компонент Toggle/Switch
|
|
||||||
- [ ] FE: Страница брендбука «Компоненты → Кнопки» с документацией
|
|
||||||
- [ ] FE: Страница брендбука «Компоненты → Форм-контролы»
|
|
||||||
- [ ] FE: Копирование HTML/CSS кода компонента в один клик
|
|
||||||
|
|
||||||
**Результат спринта:** Раздел «Базовые компоненты — кнопки и ввод» готов.
|
### Задачи — компоненты
|
||||||
|
- [x] FE: Компонент Button (`components/ui/Button.tsx`) — варианты primary/outline/teal/pill, размеры sm/md/lg, loading
|
||||||
|
- [x] FE: Компонент Input (`.bb-input`) — text, password, focus/error/disabled, height 50px как на сайте
|
||||||
|
- [x] FE: Компонент Textarea (`.bb-textarea`) — resize:vertical, min-height 100px
|
||||||
|
- [x] FE: Компонент Select (`.bb-select`) — height 50px, кастомная стрелка SVG
|
||||||
|
- [x] FE: Компонент Checkbox (`.bb-checkbox`) и Radio (`.bb-radio`)
|
||||||
|
- [x] FE: Компонент Toggle/Switch (`components/ui/Toggle.tsx`) — React "use client", defaultChecked/disabled/label
|
||||||
|
- [x] FE: Страница `/components/buttons` — 4 варианта по реальному сайту, размеры, состояния, «Где применяется», LLM-блок v2.0
|
||||||
|
- [x] FE: Страница `/components/forms` — все 6 контролов, контекст на сайте с макетом, LLM-блок v2.0
|
||||||
|
- [x] FE: Копирование HTML/CSS кода в один клик (`components/ui/CodeCopy.tsx`)
|
||||||
|
|
||||||
|
### Фактические результаты
|
||||||
|
- **4 варианта кнопок** — скопированы с реального сайта perm.oclinica.ru: primary(#FFA39C), outline(#BF9975), teal(#60959c), pill(#e9e4d4)
|
||||||
|
- **6 форм-контролов** — input/textarea/select/checkbox/radio/toggle с полной документацией состояний
|
||||||
|
- **Input/Select** — height 50px, border 1px solid #ccc, border-radius 4px (entityform CSS с реального сайта)
|
||||||
|
- **Макет формы** на фоне #b8e6ed как «Узнайте стоимость операции» на oclinica.ru/lor
|
||||||
|
- **LLM-блоки** добавлены на логотип, цвета, типографику, кнопки, форм-контролы
|
||||||
|
- **Компоненты:** Button.tsx, Toggle.tsx, CodeCopy.tsx в `components/ui/`
|
||||||
|
- **Sidebar:** Sprint 3 · v0.3.0, кнопки/формы убраны из «скоро»
|
||||||
|
- **Деплой:** https://web-oclinica.vercel.app (production)
|
||||||
|
|
||||||
|
**Результат спринта:** Раздел «Базовые компоненты» полностью готов. Стили соответствуют реальному сайту.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 4 — Карточки (Cards)
|
## Sprint 4 — Карточки (Cards) ✅ ЗАВЕРШЁН
|
||||||
|
|
||||||
**Цель:** Все типы карточек, используемых на сайте.
|
**Цель:** Все типы карточек, используемых на сайте.
|
||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
- [ ] FE: Карточка врача (DoctorCard) — фото, имя, специализация, кнопка записи
|
- [x] Docs: Обновить `docs/LLM_CONTEXT.md` → версия 4.0
|
||||||
- [ ] FE: Карточка услуги / заболевания
|
- [x] FE: Добавить LLM-блок на страницу «Карточки» — v1.0
|
||||||
- [ ] FE: Карточка новости — превью, дата, заголовок, анонс, читать далее
|
- [x] FE: Карточка врача (DoctorCard) — фото 110×160px, имя, специализация, стаж, кнопка outline
|
||||||
- [ ] FE: Карточка отзыва — автор, текст, рейтинг (звёзды), дата
|
- [x] FE: Карточка услуги / заболевания (ServiceCard) — иконка + заголовок + описание
|
||||||
- [ ] FE: Карточка цены — услуга, стоимость, описание
|
- [x] FE: Карточка новости — превью, дата, заголовок, анонс, hover #eef4d1 (CSS сайта)
|
||||||
- [ ] FE: Бейджи, теги, алерты (inline и toast)
|
- [x] FE: Карточка отзыва — рейтинг SVG-звёзды, 4-строчный clamp, фон #eef4d1
|
||||||
- [ ] FE: Страница «Компоненты → Карточки» с документацией
|
- [x] FE: Карточка цены — услуга + стоимость + highlighted-вариант
|
||||||
|
- [x] FE: Бейджи (6 вариантов), теги (default/active), алерты (info/success/warning/error)
|
||||||
|
- [x] FE: Страница `/components/cards` с документацией и LLM-блоком
|
||||||
|
|
||||||
**Результат спринта:** Все карточки задокументированы и показаны в брендбуке.
|
### Фактические результаты
|
||||||
|
- **5 типов карточек:** DoctorCard, NewsCard, ReviewCard, PriceCard, ServiceCard
|
||||||
|
- **NewsCard hover** — bg `#eef4d1` + box-shadow `0 0 16px #9e9e9a` (1:1 с реальным CSS сайта)
|
||||||
|
- **ReviewCard** — фон `#eef4d1` из CSS сайта, WebkitLineClamp: 4
|
||||||
|
- **Бейджи** — 6 вариантов (primary/success/warning/danger/neutral/outline-blue)
|
||||||
|
- **Алерты** — info (#dff0fa), success (#d1fae5), warning (#fef3c7), error (#fee2e2)
|
||||||
|
- **Исправлены цвета Oracal** — точные RGB из каталога для всех 6 плёнок
|
||||||
|
- **Деплой:** https://web-oclinica.vercel.app (production)
|
||||||
|
|
||||||
|
**Результат спринта:** Все карточки задокументированы. Цвета Oracal исправлены по каталогу.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,6 +176,8 @@
|
|||||||
**Цель:** Ключевые верхние блоки страниц.
|
**Цель:** Ключевые верхние блоки страниц.
|
||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
|
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации Hero и CEO-блоков
|
||||||
|
- [ ] FE: Добавить LLM-блоки на страницы Hero и CEO-текст
|
||||||
- [ ] FE: Hero-блок вариант 1 — фон + заголовок + CTA
|
- [ ] FE: Hero-блок вариант 1 — фон + заголовок + CTA
|
||||||
- [ ] FE: Hero-блок вариант 2 — иллюстрация сбоку
|
- [ ] FE: Hero-блок вариант 2 — иллюстрация сбоку
|
||||||
- [ ] FE: Hero-блок вариант 3 — с встроенной формой записи
|
- [ ] FE: Hero-блок вариант 3 — с встроенной формой записи
|
||||||
@@ -131,6 +195,8 @@
|
|||||||
**Цель:** Блоки и компоненты, связанные с врачами.
|
**Цель:** Блоки и компоненты, связанные с врачами.
|
||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
|
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации блоков врачей
|
||||||
|
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Врачи»
|
||||||
- [ ] FE: Блок «Наши врачи» — сетка карточек с фильтром по специализации
|
- [ ] FE: Блок «Наши врачи» — сетка карточек с фильтром по специализации
|
||||||
- [ ] FE: Блок «Врач — профиль» (полная страница): фото, биография, специализации, расписание
|
- [ ] FE: Блок «Врач — профиль» (полная страница): фото, биография, специализации, расписание
|
||||||
- [ ] FE: Компонент «Расписание / слоты записи»
|
- [ ] FE: Компонент «Расписание / слоты записи»
|
||||||
@@ -147,6 +213,8 @@
|
|||||||
**Цель:** Контентные блоки сайта.
|
**Цель:** Контентные блоки сайта.
|
||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
|
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации блоков отзывов и новостей
|
||||||
|
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Отзывы и новости»
|
||||||
- [ ] FE: Блок отзывов — карусель
|
- [ ] FE: Блок отзывов — карусель
|
||||||
- [ ] FE: Блок отзывов — статичная сетка
|
- [ ] FE: Блок отзывов — статичная сетка
|
||||||
- [ ] FE: Блок рейтинга (звёзды + количество отзывов)
|
- [ ] FE: Блок рейтинга (звёзды + количество отзывов)
|
||||||
@@ -165,6 +233,8 @@
|
|||||||
**Цель:** Все формы и диалоги взаимодействия с пациентом.
|
**Цель:** Все формы и диалоги взаимодействия с пациентом.
|
||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
|
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации форм и модальных окон
|
||||||
|
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Формы и контакты»
|
||||||
- [ ] FE: Форма записи — короткая (имя, телефон)
|
- [ ] FE: Форма записи — короткая (имя, телефон)
|
||||||
- [ ] FE: Форма записи — расширенная (имя, телефон, специализация, врач, дата)
|
- [ ] FE: Форма записи — расширенная (имя, телефон, специализация, врач, дата)
|
||||||
- [ ] FE: Форма записи в модальном окне
|
- [ ] FE: Форма записи в модальном окне
|
||||||
@@ -226,20 +296,25 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 12 — Деплой, полировка, документация
|
## Sprint 12 — Полировка, финальный деплой и документация
|
||||||
|
|
||||||
**Цель:** Финальный релиз и публикация.
|
**Цель:** Финальный релиз. Фронтенд уже живёт на Vercel с Sprint 2, Sprint 12 — финальная полировка и production-готовность бэкенда.
|
||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
- [ ] BE + FE: Полный smoke-тест всего брендбука
|
- [ ] BE + FE: Полный smoke-тест всего брендбука
|
||||||
- [ ] FE: Мобильная адаптация — финальная проверка всех страниц
|
- [ ] FE: Мобильная адаптация — финальная проверка всех страниц
|
||||||
- [ ] FE: Accessibility-аудит (WCAG AA)
|
- [ ] FE: Accessibility-аудит (WCAG AA)
|
||||||
- [ ] Деплой: настройка CI/CD, публикация на сервере
|
- [ ] Деплой BE: выбрать и настроить хостинг для NestJS + PostgreSQL
|
||||||
- [ ] Docs: Создание `docs/deployment.md`
|
- [ ] Деплой: настроить автоматический деплой через Gitea Actions → Vercel (при пуше в `main`)
|
||||||
- [ ] Docs: Обновление README.md финальными инструкциями
|
- [ ] Docs: Обновление `docs/DEPLOY.md` финальными инструкциями
|
||||||
- [ ] Design: Финальный ревью брендбука
|
- [ ] Design: Финальный ревью брендбука
|
||||||
|
|
||||||
**Результат спринта:** Брендбук задеплоен и доступен по URL.
|
### Текущий статус деплоя
|
||||||
|
- **Фронтенд:** https://web-oclinica.vercel.app (Vercel Hobby, задеплоен в Sprint 2)
|
||||||
|
- **Команда деплоя:** `cd apps/web && vercel --prod --yes`
|
||||||
|
- **Бэкенд:** локально (Docker Compose), хостинг выбирается в Sprint 12
|
||||||
|
|
||||||
|
**Результат спринта:** Брендбук полностью готов, оба сервиса задеплоены, автодеплой настроен.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,10 @@
|
|||||||
| Краткое название | oclinica-brandbook |
|
| Краткое название | oclinica-brandbook |
|
||||||
| Сайт клиники | https://oclinica.ru |
|
| Сайт клиники | https://oclinica.ru |
|
||||||
| Тип системы | Веб-приложение (Living Styleguide) |
|
| Тип системы | Веб-приложение (Living Styleguide) |
|
||||||
| Режим работы | Локальная разработка + деплой на сервер |
|
| Режим работы | Локальная разработка + Vercel (preview + production) |
|
||||||
| Аудитория | Внутренние дизайнеры клиники, внешние подрядчики |
|
| Аудитория | Внутренние дизайнеры клиники, внешние подрядчики |
|
||||||
| Хостинг | TBD — будет прописан отдельно |
|
| Хостинг (фронтенд) | Vercel Hobby (бесплатно) — https://web-oclinica.vercel.app |
|
||||||
|
| Хостинг (бэкенд + БД) | TBD — уточняется при переходе к Sprint 11 (экспериментальная секция) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -152,6 +153,17 @@
|
|||||||
- HTML-код компонента с кнопкой «Скопировать»
|
- HTML-код компонента с кнопкой «Скопировать»
|
||||||
- Краткое описание и правила применения
|
- Краткое описание и правила применения
|
||||||
|
|
||||||
|
**ФТ-03-LLM.** На каждой странице брендбука, содержащей дизайн-стандарты (цвета, типографика,
|
||||||
|
компоненты, блоки, страницы), должен присутствовать **LLM-блок** — свёрнутая или отдельная
|
||||||
|
секция с машиночитаемым описанием стандарта для использования языковыми моделями при
|
||||||
|
разработке дизайна, макетов, сайта и приложений. LLM-блок содержит:
|
||||||
|
- Все токены и значения в табличном формате
|
||||||
|
- Правила применения в формате «разрешено / запрещено»
|
||||||
|
- Взаимосвязи с другими стандартами (ссылки на разделы)
|
||||||
|
- Примеры корректного и некорректного использования
|
||||||
|
|
||||||
|
Сводный LLM-контекст всего брендбука хранится в файле: **`docs/LLM_CONTEXT.md`**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.2 Раздел «Цвета»
|
### 5.2 Раздел «Цвета»
|
||||||
@@ -393,17 +405,18 @@
|
|||||||
|
|
||||||
| Слой | Технология | Версия | Обоснование |
|
| Слой | Технология | Версия | Обоснование |
|
||||||
|-------------------|-----------------------|----------|--------------------------------------------------|
|
|-------------------|-----------------------|----------|--------------------------------------------------|
|
||||||
| Фронтенд | Next.js (App Router) | 15.x | SSR/SSG, оптимизация, экосистема React |
|
| Фронтенд | Next.js (App Router) | 16.x | SSR/SSG, оптимизация, экосистема React |
|
||||||
| Бэкенд | NestJS | 10.x | Типизированный Node.js фреймворк, DI, модули |
|
| Бэкенд | NestJS | 11.x | Типизированный Node.js фреймворк, DI, модули |
|
||||||
| База данных | PostgreSQL | 16.x | Надёжная реляционная БД, JSON-поля для атрибутов |
|
| База данных | PostgreSQL | 16.x | Надёжная реляционная БД, JSON-поля для атрибутов |
|
||||||
| ORM | Prisma | 5.x | Type-safe запросы, миграции, seed |
|
| ORM | Prisma | 7.x | Type-safe запросы, миграции, seed |
|
||||||
| Стилизация | CSS Modules | — | Изоляция стилей, нет рантайм-зависимостей |
|
| Стилизация | Tailwind CSS | 4.x | Utility-first, CSS-переменные, нет рантайм-overhead |
|
||||||
| Дизайн-токены | CSS Custom Properties | — | Нативно поддерживается всеми браузерами |
|
| Дизайн-токены | CSS Custom Properties | — | Нативно поддерживается всеми браузерами |
|
||||||
| Шрифт (бренд) | DINPro | — | Фирменный шрифт бренда, оффлайн-носители |
|
| Шрифт (бренд) | DINPro | — | Фирменный шрифт бренда, оффлайн-носители |
|
||||||
| Шрифт (веб) | Fira Sans | — | Google Fonts, кириллица, веса 300 и 400, сайт |
|
| Шрифт (веб) | Fira Sans | — | Google Fonts, кириллица, веса 300/400/500/600 |
|
||||||
| Авторизация | JWT + httpOnly cookie | — | Безопасное хранение токена |
|
| Авторизация | JWT + httpOnly cookie | — | Безопасное хранение токена |
|
||||||
| Пакетный менеджер | pnpm | 9.x | Monorepo workspaces, скорость |
|
| Пакетный менеджер | pnpm | 10.x | Monorepo workspaces, скорость |
|
||||||
| Контейнеризация | Docker + Compose | — | Единообразное окружение dev/prod |
|
| Контейнеризация | Docker + Compose | — | Единообразное окружение локальной разработки |
|
||||||
|
| Хостинг фронтенда | Vercel | — | Нативная поддержка Next.js, бесплатный Hobby-план |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -480,16 +493,29 @@ oclinica-brandbook/
|
|||||||
| Docker | >= 24 |
|
| Docker | >= 24 |
|
||||||
| Docker Compose | >= 2 |
|
| Docker Compose | >= 2 |
|
||||||
|
|
||||||
### 12.2 Production-сервер
|
### 12.2 Деплой фронтенда (Vercel)
|
||||||
|
|
||||||
> TBD — параметры хостинга будут прописаны отдельно.
|
Фронтенд (`apps/web`) деплоится на Vercel Hobby (бесплатно).
|
||||||
|
|
||||||
Минимальные ожидаемые требования:
|
**Production URL:** https://web-oclinica.vercel.app
|
||||||
- ОС: Ubuntu 22.04+
|
|
||||||
- RAM: 2 GB
|
**Команда деплоя** (из директории `apps/web`):
|
||||||
- Disk: 20 GB
|
```bash
|
||||||
- PostgreSQL 16 (или managed database)
|
vercel --prod --yes
|
||||||
- Node.js 20 LTS
|
```
|
||||||
|
|
||||||
|
**Требования:**
|
||||||
|
- Vercel CLI установлен глобально: `npm install -g vercel`
|
||||||
|
- Выполнен `vercel login` (однократно)
|
||||||
|
|
||||||
|
**Деплой занимает ~30 секунд.** После команды изменения сразу доступны по production URL.
|
||||||
|
|
||||||
|
### 12.3 Бэкенд и база данных
|
||||||
|
|
||||||
|
> TBD — параметры хостинга бэкенда (NestJS + PostgreSQL) будут определены к Sprint 11,
|
||||||
|
> когда потребуется работающий API для экспериментальной секции.
|
||||||
|
|
||||||
|
Варианты для рассмотрения: Railway, Render, VPS клиники.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -511,11 +537,12 @@ oclinica-brandbook/
|
|||||||
| № | Вопрос | Ответственный | Срок |
|
| № | Вопрос | Ответственный | Срок |
|
||||||
|----|--------------------------------------------------------------------------------------|---------------|----------|
|
|----|--------------------------------------------------------------------------------------|---------------|----------|
|
||||||
| ОВ-01 | Доступен ли JSON API или REST API на oclinica.ru? Каков формат ответов? | Клиника | Sprint 1 |
|
| ОВ-01 | Доступен ли JSON API или REST API на oclinica.ru? Каков формат ответов? | Клиника | Sprint 1 |
|
||||||
| ОВ-02 | Параметры хостинга для production-деплоя | Клиника | TBD |
|
| ОВ-02 | ~~Параметры хостинга для production-деплоя~~ **Закрыт:** фронтенд — Vercel Hobby (https://web-oclinica.vercel.app); бэкенд — TBD к Sprint 11 | — | Частично закрыт |
|
||||||
| ОВ-03 | Нужна ли страница «Заболевание» как отдельный тип, или это подвид страницы «Услуга»? | Клиника | Sprint 9 |
|
| ОВ-03 | Нужна ли страница «Заболевание» как отдельный тип, или это подвид страницы «Услуга»? | Клиника | Sprint 9 |
|
||||||
| ОВ-04 | Список иконок — какую стороннюю библиотеку утвердить? (Lucide, Heroicons, и др.) | Совместно | Sprint 2 |
|
| ОВ-04 | Список иконок — какую стороннюю библиотеку утвердить? (Lucide, Heroicons, и др.) | Совместно | Sprint 2 |
|
||||||
| ОВ-05 | ~~Нужен ли раздел «Логотип» в v1.0 или ждём вектор?~~ **Закрыт:** страница логотипа реализуется в Sprint 1 с PNG-версией; вектор будет добавлен позже | — | Закрыт |
|
| ОВ-05 | ~~Нужен ли раздел «Логотип» в v1.0 или ждём вектор?~~ **Закрыт:** страница логотипа реализуется в Sprint 1 с PNG-версией; вектор будет добавлен позже | — | Закрыт |
|
||||||
| ОВ-06 | HEX-эквиваленты цветов Oracal (053M, 073M, 066M, 050M, 081M, 080M) для использования в токенах | Совместно | Sprint 2 |
|
| ОВ-06 | ~~HEX-эквиваленты цветов Oracal~~ **Закрыт:** приблизительные HEX зафиксированы в Sprint 2 (053M=#7ecfca, 073M=#5b7b87, 066M=#5bb5ad, 050M=#1b4c72, 081M=#c4a882, 080M=#5c2e0e). Дополнительно — реальные цвета сайта извлечены из CSS (см. ОВ-07). Точная калибровка Oracal — при получении физических образцов. | — | Закрыт |
|
||||||
|
| ОВ-07 | **Цвета сайта oclinica.ru** — CSS тема Drupal доступна по адресу: `https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css`. Тема: `clinic_bootstrap_mobile`. Ключевые цвета извлечены парсингом (python + regex + Counter), 2026-03-22. Добавлены в раздел «Цвета с сайта» в брендбуке (`/foundation/colors`). Расхождение с Oracal-палитрой ожидаемо — цифровые адаптации под экран. | — | Закрыт |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "oclinica-brandbook",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "pnpm --parallel -r dev",
|
||||||
|
"build": "pnpm --parallel -r build",
|
||||||
|
"lint": "pnpm --parallel -r lint"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20",
|
||||||
|
"pnpm": ">=9"
|
||||||
|
}
|
||||||
|
}
|
||||||