Compare commits

3 Commits
43 changed files with 285 additions and 4229 deletions
+3 -3
View File
@@ -1,4 +1,4 @@
title: 'Title example'
title: "Title example"
project:
org: 'example'
repo: 'example'
org: "example"
repo: "example"
-1
View File
@@ -1 +0,0 @@
yarn lint-staged
-7
View File
@@ -1,7 +0,0 @@
dist
coverage
*.d.ts
node_modules
.idea
logs
report
-29
View File
@@ -1,29 +0,0 @@
{
"printWidth": 100,
"useTabs": false,
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"singleAttributePerLine": true,
"overrides": [
{
"files": [
"*.json"
],
"options": {
"printWidth": 10
}
},
{
"files": [
"*.md",
"*.mdx"
],
"options": {
"proseWrap": "always"
}
}
]
}
-47
View File
@@ -1,47 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
repository.
## Project Overview
Docuservix docs — шаблон документационного сайта на Docusaurus 3.10 (React 19, TypeScript 6).
Конфигурация сайта читается из `.docuservix.yml` (title, project.org, project.repo, dirs). Локаль —
русский (`ru`).
## Commands
Используется **yarn**.
- `yarn start` — dev-сервер
- `yarn build` — production-сборка в `build/`
- `yarn typecheck` — проверка типов (tsc)
- `yarn prettier:check` — проверка форматирования
- `yarn prettier:fix` — автоформатирование
## Architecture
- `docusaurus.config.ts` — главный конфиг; читает `.docuservix.yml` через `js-yaml`
- `src/pages/` — кастомные страницы (index.tsx — главная)
- `src/css/custom.css` — глобальные CSS-переменные (`--ifm-*`)
- `docs/` — Markdown/MDX-документация
- `blog/` — блог (опционально, включается через `dirs.blog` в `.docuservix.yml`)
- Mermaid-диаграммы включены (`@docusaurus/theme-mermaid`)
- Docusaurus future v4 compatibility flag включён
## Code Style
- Prettier: 4 пробела, single quotes, trailing commas, `printWidth: 100`,
`singleAttributePerLine: true`
- JSON: `printWidth: 10` (каждое свойство на отдельной строке)
- Markdown/MDX: `proseWrap: always`
- Husky + lint-staged: prettier запускается автоматически на pre-commit
- CSS Modules (`*.module.css`) с camelCase именами классов
- **Без default export** в shared/UI компонентах; default export допустим только для Docusaurus
route-компонентов (page components)
## Environment
- Node >= 20
- Env vars: `DOCUSERVIX_URL` (production URL), `DOCUSERVIX_ON_BROKEN_LINKS` (override onBrokenLinks)
- Gitea instance: `git.jt4d.ru`
+2 -4
View File
@@ -14,8 +14,7 @@ yarn
yarn start
```
This command starts a local development server and opens up a browser window. Most changes are
reflected live without having to restart the server.
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
## Build
@@ -23,5 +22,4 @@ reflected live without having to restart the server.
yarn build
```
This command generates static content into the `build` directory and can be served using any static
contents hosting service.
This command generates static content into the `build` directory and can be served using any static contents hosting service.
+104 -104
View File
@@ -2,120 +2,120 @@ name: 'Docusaurus Deploy'
description: 'Builds Docusaurus docs from repo and deploys to S3'
inputs:
docs-path:
description: 'Path to docs directory in calling repo'
default: 'docs'
on-broken-links:
description: 'Behavior on broken links: throw, warn, or ignore'
default: 'throw'
prefix:
description: 'Prefix for S3 path'
default: ''
docs-path:
description: 'Path to docs directory in calling repo'
default: 'docs'
on-broken-links:
description: 'Behavior on broken links: throw, warn, or ignore'
default: 'throw'
prefix:
description: 'Additional prefix for S3 path'
default: ''
runs:
using: 'composite'
steps:
- name: Compute target URL
shell: bash
run: |
REF="${{ github.head_ref || github.ref_name }}"
REPO="${{ github.event.repository.name }}"
ORG="${{ github.repository_owner }}"
using: 'composite'
steps:
- name: Compute target URL
shell: bash
run: |
REF="${{ github.head_ref || github.ref_name }}"
REPO="${{ github.event.repository.name }}"
ORG="${{ github.repository_owner }}"
if [[ "$REF" == "main" || "$REF" == "master" ]]; then
URL="http://${REPO}.${ORG}.jt4d-wiki.ru.net"
S3_PATH="${ORG}.${REPO}"
else
URL="http://${REF}.${REPO}.${ORG}.jt4d-wiki.ru.net"
S3_PATH="${ORG}.${REPO}.${REF}"
fi
if [[ "$REF" == "main" || "$REF" == "master" ]]; then
URL="http://${REPO}.${ORG}.jt4d-wiki.ru.net"
S3_PATH="${ORG}.${REPO}"
else
URL="http://${REF}.${REPO}.${ORG}.jt4d-wiki.ru.net"
S3_PATH="${ORG}.${REPO}.${REF}"
fi
PREFIX="${{ inputs.prefix }}"
if [[ -n "$PREFIX" ]]; then
S3_PATH="${PREFIX}/${S3_PATH}"
fi
PREFIX="${{ inputs.prefix }}"
if [[ -n "$PREFIX" ]]; then
S3_PATH="${PREFIX}/${S3_PATH}"
fi
echo "TARGET_URL=$URL" >> $GITHUB_ENV
echo "S3_PATH=$S3_PATH" >> $GITHUB_ENV
echo "TARGET_URL=$URL" >> $GITHUB_ENV
echo "S3_PATH=$S3_PATH" >> $GITHUB_ENV
- name: Set docs status pending
shell: bash
run: |
curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"state": "pending",
"context": "Docs",
"description": "building",
"target_url": "${{ env.TARGET_URL }}"
}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.sha }}"
- name: Set docs status pending
shell: bash
run: |
curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"state": "pending",
"context": "Docs",
"description": "building",
"target_url": "${{ env.TARGET_URL }}"
}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.sha }}"
- name: Copy docs into Docusaurus
shell: bash
run: |
DOCUSAURUS_DIR="${{ github.action_path }}"
rm -rf "${DOCUSAURUS_DIR}/docs"
cp -r "${{ inputs.docs-path }}" "${DOCUSAURUS_DIR}/docs"
cp "${{ github.workspace }}/.docuservix.yml" "${DOCUSAURUS_DIR}"
- name: Copy docs into Docusaurus
shell: bash
run: |
DOCUSAURUS_DIR="${{ github.action_path }}"
rm -rf "${DOCUSAURUS_DIR}/docs"
cp -r "${{ inputs.docs-path }}" "${DOCUSAURUS_DIR}/docs"
cp "${{ github.workspace }}/.docuservix.yml" "${DOCUSAURUS_DIR}"
- name: Prepare docs
shell: bash
working-directory: ${{ github.action_path }}
run: node scripts/prepare-docs.mjs
- name: Prepare docs
shell: bash
working-directory: ${{ github.action_path }}
run: node scripts/prepare-docs.mjs
- name: Install Docusaurus dependencies
shell: bash
working-directory: ${{ github.action_path }}
run: yarn install --frozen-lockfile
- name: Install Docusaurus dependencies
shell: bash
working-directory: ${{ github.action_path }}
run: yarn install --frozen-lockfile
- name: Build docs
shell: bash
working-directory: ${{ github.action_path }}
env:
DOCUSERVIX_ON_BROKEN_LINKS: ${{ inputs.on-broken-links }}
DOCUSERVIX_URL: ${{ env.TARGET_URL }}
run: yarn docusaurus build --out-dir ${{ github.workspace }}/generated-docs
- name: Build docs
shell: bash
working-directory: ${{ github.action_path }}
env:
DOCUSERVIX_ON_BROKEN_LINKS: ${{ inputs.on-broken-links }}
DOCUSERVIX_URL: ${{ env.TARGET_URL }}
run: yarn docusaurus build --out-dir ${{ github.workspace }}/generated-docs
- name: Upload to S3
shell: bash
env:
AWS_ACCESS_KEY_ID: ${{ vars.DOCUSERVIX_S3_ACCESS }}
AWS_SECRET_ACCESS_KEY: ${{ vars.DOCUSERVIX_S3_SECRET }}
run: |
aws s3 sync generated-docs/ \
s3://${{ vars.DOCUSERVIX_S3_BUCKET }}/${{ env.S3_PATH }}\
--endpoint-url ${{ vars.DOCUSERVIX_S3_URL }} \
--acl public-read \
--delete
- name: Upload to S3
shell: bash
env:
AWS_ACCESS_KEY_ID: ${{ vars.DOCUSERVIX_S3_ACCESS }}
AWS_SECRET_ACCESS_KEY: ${{ vars.DOCUSERVIX_S3_SECRET }}
run: |
aws s3 sync generated-docs/ \
s3://${{ vars.DOCUSERVIX_S3_BUCKET }}/${{ env.S3_PATH }}\
--endpoint-url ${{ vars.DOCUSERVIX_S3_URL }} \
--acl public-read \
--delete
- name: Set docs status success
if: success()
shell: bash
run: |
curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"state": "success",
"context": "Docs",
"description": "deployed",
"target_url": "${{ env.TARGET_URL }}"
}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.sha }}"
- name: Set docs status success
if: success()
shell: bash
run: |
curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"state": "success",
"context": "Docs",
"description": "deployed",
"target_url": "${{ env.TARGET_URL }}"
}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.sha }}"
- name: Set docs status failure
if: failure()
shell: bash
run: |
curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"state": "failure",
"context": "Docs",
"description": "build failed",
"target_url": "${{ env.TARGET_URL }}"
}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.sha }}"
- name: Set docs status failure
if: failure()
shell: bash
run: |
curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"state": "failure",
"context": "Docs",
"description": "build failed",
"target_url": "${{ env.TARGET_URL }}"
}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.sha }}"
-67
View File
@@ -1,70 +1,3 @@
# Добро пожаловать в Docuservix!
Вам надо настроить публикацию документации по инструкции в https://git.jt4d.ru/jt4d/docuservix
---
```mermaid
classDiagram
class IApi
class ProductService {
+IProduct[] getProducts
}
ProductService --> IApi
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
```
Для современного мира высококачественный прототип будущего проекта предполагает независимые способы
реализации существующих финансовых и административных условий. Идейные соображения высшего порядка,
а также начало повседневной работы по формированию позиции обеспечивает широкому кругу
(специалистов) участие в формировании существующих финансовых и административных условий.
Безусловно, курс на социально-ориентированный национальный проект прекрасно подходит для реализации
глубокомысленных рассуждений. Каждый из нас понимает очевидную вещь: консультация с широким активом
играет определяющее значение для модели развития. Не следует, однако, забывать, что выбранный нами
инновационный путь создаёт предпосылки для форм воздействия.
Есть над чем задуматься: реплицированные с зарубежных источников, современные исследования призывают
нас к новым свершениям, которые, в свою очередь, должны быть подвергнуты целой серии независимых
исследований. Приятно, граждане, наблюдать, как многие известные личности будут обнародованы.
Следует отметить, что повышение уровня гражданского сознания играет определяющее значение для
экспериментов, поражающих по своей масштабности и грандиозности.
Безусловно, внедрение современных методик создаёт необходимость включения в производственный план
целого ряда внеочередных мероприятий с учётом комплекса позиций, занимаемых участниками в отношении
поставленных задач. Кстати, акционеры крупнейших компаний разоблачены. В своём стремлении улучшить
пользовательский опыт мы упускаем, что диаграммы связей, превозмогая сложившуюся непростую
экономическую ситуацию, разоблачены.
```ts
/**
* Очень важный класс
*/
export class Foo {
bar(value: number): number {
console.log('Bar: ' + value);
return value;
}
}
```
+23 -29
View File
@@ -1,12 +1,9 @@
import fs from 'fs';
import type * as Preset from '@docusaurus/preset-classic';
import type { NavbarItem } from '@docusaurus/theme-common';
import type { Config } from '@docusaurus/types';
import yaml from 'js-yaml';
import { themes as prismThemes } from 'prism-react-renderer';
import docuservix from './plugins/docuservix';
import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
import type {NavbarItem} from '@docusaurus/theme-common'
interface DocsConfig {
title: string;
@@ -16,17 +13,24 @@ interface DocsConfig {
const docsConfig = yaml.load(fs.readFileSync('./.docuservix.yml', 'utf8')) as DocsConfig;
const { title } = docsConfig;
const {
title,
} = docsConfig
const url = process.env.DOCUSERVIX_URL || 'http://example.com';
const url = process.env.DOCUSERVIX_URL;
const { org, repo } = docsConfig.project;
const {
org,
repo
} = docsConfig.project
const { docs: _docsDir = 'docs', blog: blogDir } = docsConfig.dirs || {};
const {
docs: docsDir = 'docs',
blog: blogDir
} = docsConfig.dirs || {}
const giteaUrl = 'https://git.jt4d.ru';
const onBrokenLinks =
(process.env.DOCUSERVIX_ON_BROKEN_LINKS as Config['onBrokenLinks']) || 'throw';
const onBrokenLinks = (process.env.DOCUSERVIX_ON_BROKEN_LINKS as Config['onBrokenLinks']) || 'throw';
const config: Config = {
title,
@@ -35,7 +39,6 @@ const config: Config = {
markdown: {
mermaid: true,
},
plugins: [docuservix()],
themes: ['@docusaurus/theme-mermaid'],
// Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future
@@ -104,13 +107,11 @@ const config: Config = {
label: 'Документация',
position: 'left',
},
blogDir
? {
to: '/blog',
label: 'Блог',
position: 'left',
}
: undefined,
blogDir ? {
to: '/blog',
label: 'Блог',
position: 'left'
} : undefined,
{
href: `${giteaUrl}/${org}/${repo}`,
label: 'Gitea',
@@ -120,19 +121,12 @@ const config: Config = {
},
footer: {
style: 'dark',
copyright:
'Проект хостится на JT4D.ru, документация собрана с использованием Docuservix и Docusaurus.',
copyright: `Проект хостится на JT4D.ru, документация собрана с использованием Docuservix и Docusaurus.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
mermaid: {
theme: {
light: 'default',
dark: 'base',
},
},
} satisfies Preset.ThemeConfig,
};
-351
View File
@@ -1,351 +0,0 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { fixupPluginRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import etc from 'eslint-plugin-etc';
import _import from 'eslint-plugin-import';
import noOnlyTests from 'eslint-plugin-no-only-tests';
import noSkipTests from 'eslint-plugin-no-skip-tests';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import unusedImports from 'eslint-plugin-unused-imports';
import globals from 'globals';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: [
'**/.eslintrc.js',
'**/node_modules',
'**/coverage',
'**/build',
'**/.docusaurus',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
],
},
...compat.extends(
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'prettier',
'plugin:eslint-comments/recommended',
),
{
plugins: {
import: fixupPluginRules(_import),
react,
'react-hooks': fixupPluginRules(reactHooks),
'@typescript-eslint': typescriptEslint,
etc,
'no-only-tests': noOnlyTests,
'no-skip-tests': noSkipTests,
'unused-imports': unusedImports,
},
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parser: tsParser,
ecmaVersion: 6,
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
modules: true,
},
},
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts', '.tsx', '.json'],
},
typescript: {
alwaysTryTypes: true,
},
},
react: {
version: 'detect',
},
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
curly: ['error', 'all'],
'max-params': 'off',
'no-console': [
'error',
{
allow: ['warn', 'error'],
},
],
'no-warning-comments': [
'error',
{
terms: ['fixme'],
location: 'anywhere',
},
],
'no-unused-vars': 'off',
'space-before-blocks': 'error',
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: '*',
next: ['break', 'continue', 'return'],
},
{
blankLine: 'always',
prev: ['const', 'let'],
next: '*',
},
{
blankLine: 'any',
prev: ['const', 'let'],
next: ['const', 'let'],
},
{
blankLine: 'always',
prev: 'directive',
next: '*',
},
{
blankLine: 'any',
prev: 'directive',
next: 'directive',
},
{
blankLine: 'always',
prev: 'block-like',
next: '*',
},
{
blankLine: 'always',
prev: '*',
next: 'block-like',
},
],
'import/order': [
'error',
{
pathGroups: [
{
pattern: 'react,bem-css-modules',
group: 'builtin',
position: 'before',
},
{
pattern: '@docuservix/**',
group: 'internal',
},
],
pathGroupsExcludedImportTypes: ['react'],
'newlines-between': 'always',
groups: ['builtin', 'external', 'internal', 'parent', ['sibling', 'index']],
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
'react/no-direct-mutation-state': 'error',
'react/no-deprecated': 'error',
'react/no-unsafe': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-uses-react': 'error',
'react/jsx-curly-brace-presence': ['error', 'never'],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
quotes: [
'error',
'single',
{
avoidEscape: true,
},
],
'quote-props': ['warn', 'as-needed'],
'@typescript-eslint/no-explicit-any': [
'warn',
{
ignoreRestArgs: true,
},
],
'@typescript-eslint/member-ordering': [
'error',
{
default: [
'public-static-field',
'protected-static-field',
'private-static-field',
'public-instance-field',
'protected-instance-field',
'private-instance-field',
'constructor',
'public-instance-method',
'protected-instance-method',
'private-instance-method',
'public-static-method',
'protected-static-method',
'private-static-method',
'signature',
],
},
],
'etc/prefer-interface': [
'warn',
{
allowLocal: true,
},
],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description',
'ts-nocheck': 'allow-with-description',
'ts-check': false,
'ts-expect-error': false,
},
],
'@typescript-eslint/no-empty-interface': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-unused-vars': 'off',
// todo изучить и включить
'@typescript-eslint/no-unused-expressions': 'off',
// todo изучить и включить
'@typescript-eslint/no-empty-object-type': 'off',
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@nestjs/swagger',
importNames: ['PartialType'],
message:
"Please import 'PartialType' from '@src/server/common/nest' instead.",
},
{
name: 'react-bootstrap',
importNames: [
'Card',
'CardHeader',
'CardBody',
'CardFooter',
'Row',
'Col',
'Modal',
],
message: "Please use project's components with same name",
},
{
name: 'react-bootstrap/Modal',
message: "Please use project's components with same name",
},
{
name: '@nestjs/common',
importNames: ['Logger'],
message: "Please import 'Logger' from '@src/server/logger' instead.",
},
{
name: 'nestjs-pino',
importNames: ['Logger', 'PinoLogger'],
message: "Please import 'Logger' from '@src/server/logger' instead.",
},
],
},
],
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'eslint-comments/require-description': [
'error',
{
ignore: ['eslint-enable'],
},
],
'eslint-comments/disable-enable-pair': [
'error',
{
allowWholeFile: true,
},
],
complexity: ['warn', 10],
eqeqeq: ['error'],
'func-style': ['warn', 'declaration'],
},
},
{
files: ['**/*.spec.{js,jsx,ts,tsx}'],
rules: {
'no-only-tests/no-only-tests': 'error',
'no-skip-tests/no-skip-tests': 'warn',
'no-console': [
'warn',
{
allow: ['warn', 'error'],
},
],
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
];
+50 -86
View File
@@ -1,88 +1,52 @@
{
"name": "docusaurus",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "docusaurus build",
"clear": "docusaurus clear",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"eslint:check": "yarn eslint",
"eslint:fix": "yarn eslint --fix",
"lint": "run-s eslint:fix prettier:fix",
"lint:check": "run-s eslint:check prettier:check",
"prepare": "husky",
"prettier:check": "prettier --check \"**/*.{ts,tsx,js,mjs,json,yml,yaml,md,mdx}\"",
"prettier:fix": "prettier --write \"**/*.{ts,tsx,js,mjs,json,yml,yaml,md,mdx}\"",
"serve": "docusaurus serve",
"start": "docusaurus start",
"swizzle": "docusaurus swizzle",
"typecheck": "tsc",
"write-heading-ids": "docusaurus write-heading-ids",
"write-translations": "docusaurus write-translations"
},
"lint-staged": {
"*.{json,ts,tsx,js,jsx,js,mjs,md,mdx,yaml,yml}": "prettier --write",
"{src,e2e}/**/*.{ts,tsx}": "eslint --quiet --fix"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"dependencies": {
"@docusaurus/core": "3.10.1",
"@docusaurus/faster": "3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@docusaurus/theme-mermaid": "3.10.1",
"@mdx-js/react": "^3.0.0",
"bem-css-modules": "^1.4.3",
"clsx": "^2.0.0",
"js-yaml": "^4.2.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1",
"@docusaurus/tsconfig": "3.10.1",
"@docusaurus/types": "3.10.1",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.8.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-etc": "^2.0.3",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-no-skip-tests": "^1.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-unused-imports": "^4.0.1",
"globals": "^17.6.0",
"husky": "^9.1.7",
"lint-staged": "^17.0.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.8.4",
"typescript": "~6.0.2"
},
"engines": {
"node": ">=20.0"
}
"name": "docusaurus",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.10.1",
"@docusaurus/faster": "3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@docusaurus/theme-mermaid": "3.10.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"js-yaml": "^4.2.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1",
"@docusaurus/tsconfig": "3.10.1",
"@docusaurus/types": "3.10.1",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.0.0",
"typescript": "~6.0.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=20.0"
}
}
@@ -1,59 +0,0 @@
.MD p:last-child {
margin-bottom: 0;
}
.MD p:first-child {
margin-top: 0;
}
.MD code {
padding: 0.15em 0.4em;
border-radius: 4px;
background: var(--ifm-color-emphasis-200);
font-size: 0.85em;
}
.MD pre {
margin: 0.5em 0;
padding: 0.75em;
overflow-x: auto;
border-radius: 6px;
background: var(--ifm-color-emphasis-100);
}
.MD pre code {
padding: 0;
background: none;
}
.MD ul,
.MD ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.MD table {
width: 100%;
margin: 0.5em 0;
border-collapse: collapse;
font-size: 0.9em;
}
.MD th,
.MD td {
padding: 0.4em 0.75em;
border: 1px solid var(--ifm-color-emphasis-300);
text-align: left;
}
.MD th {
background: var(--ifm-color-emphasis-100);
font-weight: var(--ifm-font-weight-semibold);
}
.MD blockquote {
margin: 0.5em 0;
padding: 0.25em 1em;
border-left: 3px solid var(--ifm-color-emphasis-300);
color: var(--ifm-color-emphasis-700);
}
@@ -1,17 +0,0 @@
import React, { ReactNode } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import styles from './MD.module.css';
interface MDProps {
children: string;
}
export function MD({ children }: MDProps): ReactNode {
return (
<div className={styles.MD}>
<Markdown remarkPlugins={[remarkGfm]}>{children}</Markdown>
</div>
);
}
@@ -1 +0,0 @@
export { MD } from './MD';
-88
View File
@@ -1,88 +0,0 @@
import { useLocation } from '@docusaurus/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useOptions } from '@docuservix/hooks/useOptions';
import { IChat, IChatMessage, IChatSource } from '@docuservix/models/chat';
interface UseChatResult {
dialog: IChat;
typing: boolean;
statusMessage?: string;
sendMessage: (text: string) => void;
}
function useQuery(): string {
const location = useLocation();
const params = new URLSearchParams(location.search);
return params.get('q') ?? '';
}
export function useChat(): UseChatResult {
const chatEndpoint = useOptions().api + '/v1/chat';
const urlQuery = useQuery();
const [messages, setMessages] = useState<IChatMessage[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const initialSentRef = useRef(false);
const messagesEndRef = useRef(messages);
messagesEndRef.current = messages;
const sendMessage = useCallback(
async (text: string) => {
const content = text.trim();
if (!content) {
return;
}
const userMessage: IChatMessage = { role: 'user', content };
const newHistory = [...messagesEndRef.current, userMessage];
setMessages(newHistory);
setLoading(true);
setError(null);
try {
const res = await fetch(chatEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: newHistory }),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data: { answer: string; sources?: IChatSource[] } = await res.json();
setMessages((prev) => [
...prev,
{ role: 'assistant', content: data.answer, sources: data.sources },
]);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ошибка при обращении к серверу');
} finally {
setLoading(false);
}
},
[chatEndpoint],
);
useEffect(() => {
if (urlQuery && !initialSentRef.current) {
initialSentRef.current = true;
sendMessage(urlQuery);
}
}, [urlQuery, sendMessage]);
return {
dialog: { messages },
typing: loading,
statusMessage: error ?? undefined,
sendMessage,
};
}
-119
View File
@@ -1,119 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useOptions } from '@docuservix/hooks/useOptions';
import { ChatStatus } from '@docuservix/models/chat';
interface UseChatStatusResult {
status: ChatStatus;
apiUrl: string;
recheck: () => void;
}
const POLL_INTERVAL_MS = 30_000;
const REQUEST_TIMEOUT_MS = 5_000;
function sameStatus(a: ChatStatus, b: ChatStatus): boolean {
if (a.kind !== b.kind) return false;
if (a.kind === 'server' && b.kind === 'server') return a.label === b.label;
return true;
}
export function useChatStatus(): UseChatStatusResult {
const apiUrl = useOptions().api ?? '';
const statusApiUrl = apiUrl + '/v1/status';
const [status, setStatus] = useState<ChatStatus>({ kind: 'connecting' });
const abortRef = useRef<AbortController | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const updateStatus = useCallback((next: ChatStatus): void => {
setStatus((prev) => (sameStatus(prev, next) ? prev : next));
}, []);
const check = useCallback(async (): Promise<void> => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, REQUEST_TIMEOUT_MS);
try {
const res = await fetch(statusApiUrl, { signal: controller.signal });
if (!res.ok) {
updateStatus({ kind: 'offline' });
return;
}
const parsed = (await res.json()) as { status?: unknown } | null | undefined;
const raw = typeof parsed?.status === 'string' ? parsed.status : null;
if (raw === null || raw.length === 0) {
updateStatus({ kind: 'offline' });
return;
}
updateStatus({
kind: 'server',
label: raw.charAt(0).toUpperCase() + raw.slice(1),
});
} catch {
if (!controller.signal.aborted || timedOut) {
updateStatus({ kind: 'offline' });
}
} finally {
clearTimeout(timeoutId);
}
}, [statusApiUrl, updateStatus]);
const scheduleNext = useCallback((): void => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(async () => {
if (document.visibilityState === 'visible') {
await check();
}
scheduleNext();
}, POLL_INTERVAL_MS);
}, [check]);
const recheck = useCallback((): void => {
updateStatus({ kind: 'connecting' });
void check();
scheduleNext();
}, [check, scheduleNext, updateStatus]);
useEffect(() => {
void check();
scheduleNext();
const onVisibilityChange = (): void => {
if (document.visibilityState === 'visible') {
void check();
scheduleNext();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);
return (): void => {
document.removeEventListener('visibilitychange', onVisibilityChange);
abortRef.current?.abort();
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
};
}, [check, scheduleNext]);
return { status, apiUrl, recheck };
}
-7
View File
@@ -1,7 +0,0 @@
import { usePluginData } from '@docusaurus/useGlobalData';
import { DocuservixOptions } from '@docuservix/models/docuservix';
export function useOptions(): DocuservixOptions {
return usePluginData('docuservix') as DocuservixOptions;
}
-39
View File
@@ -1,39 +0,0 @@
import path from 'path';
import type { LoadContext, Plugin } from '@docusaurus/types';
import { DocuservixOptions } from '@docuservix/models/docuservix';
export default function docuservix(options: Partial<DocuservixOptions> = {}) {
const api = process.env.DOCUSERVIX_API || options.api || '/api';
return function pluginDocuservix(_context: LoadContext): Plugin {
return {
name: 'docuservix',
configureWebpack() {
return {
resolve: {
alias: {
'@docuservix': path.resolve(__dirname),
},
},
};
},
async contentLoaded({ actions }) {
const { addRoute, setGlobalData } = actions;
setGlobalData({
api,
});
addRoute({
path: '/chat',
component: '@docuservix/pages/chat',
exact: true,
});
},
};
};
}
-25
View File
@@ -1,25 +0,0 @@
export type ChatStatus =
| { kind: 'connecting' }
| { kind: 'offline' }
| { kind: 'server'; label: string };
export interface IChat {
messages: IChatMessage[];
}
export interface IChatSource {
file: string;
heading: string;
anchor: string;
score: number;
}
export interface IChatMessage {
role: 'user' | 'assistant';
content: string;
sources?: IChatSource[];
}
export function sourceToUrl(file: string, anchor: string): string {
return `/${file}${anchor ? `#${anchor}` : ''}`;
}
-3
View File
@@ -1,3 +0,0 @@
export interface DocuservixOptions {
api?: string;
}
@@ -1,22 +0,0 @@
import Layout from '@theme/Layout';
import { ReactNode } from 'react';
import { useChat } from '@docuservix/hooks/useChat';
import { Chat } from '@docuservix/widgets/chat';
export function ChatPage(): ReactNode {
const { dialog, typing, statusMessage, sendMessage } = useChat();
return (
<Layout title="Чат">
<main className="container margin-vert--lg">
<Chat
dialog={dialog}
typing={typing}
statusMessage={statusMessage}
onSend={sendMessage}
/>
</main>
</Layout>
);
}
-3
View File
@@ -1,3 +0,0 @@
import { ChatPage } from './ChatPage';
export default ChatPage;
@@ -1,24 +0,0 @@
.Chat {
display: flex;
flex-direction: column;
width: 100%;
background: var(--ifm-background-color);
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: var(--ifm-global-radius);
overflow: hidden;
}
.Chat__statusMessage {
font-size: .65rem;
color: #666;
bottom: 0;
left: 0;
right: 0;
padding: .75rem 2.5rem;
background: #eee;
}
.Chat__statusMessage i {
margin-right: .25rem;
color: #667eea;
}
-37
View File
@@ -1,37 +0,0 @@
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import { IChat } from '@docuservix/models/chat';
import styles from './Chat.module.css';
import { Header } from './Header';
import { Input } from './Input';
import { Messages } from './Messages';
const b = block(styles, 'Chat');
interface ChatProps {
dialog: IChat;
typing?: boolean;
statusMessage?: string;
onSend?: (text: string) => void;
}
export function Chat({ dialog, typing, statusMessage, onSend }: ChatProps): ReactNode {
const { messages } = dialog;
return (
<div className={b()}>
<Header />
<Messages
messages={messages}
typing={typing}
/>
{statusMessage && <div className={b('statusMessage')}>{statusMessage}</div>}
<Input
disabled={typing}
onSend={onSend}
/>
</div>
);
}
@@ -1,26 +0,0 @@
.Header {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--ifm-color-emphasis-200);
}
.Header__avatar {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
flex-shrink: 0;
}
.Header__info h3 {
margin: 0 0 0.25rem;
color: var(--ifm-font-color-base);
font-size: 1.125rem;
}
@@ -1,22 +0,0 @@
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import styles from './Header.module.css';
import { RobotIcon } from './icons';
import { Status } from './Status';
const b = block(styles, 'Header');
export function Header(): ReactNode {
return (
<div className={b()}>
<div className={b('avatar')}>
<RobotIcon />
</div>
<div className={b('info')}>
<h3>AI Assistant</h3>
<Status />
</div>
</div>
);
}
@@ -1,58 +0,0 @@
.Input {
display: flex;
align-items: flex-end;
gap: 0.75rem;
padding: 1rem 1.25rem;
border-top: 1px solid var(--ifm-color-emphasis-200);
}
.Input__field {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid var(--ifm-color-emphasis-200);
border-radius: 1.5rem;
font-size: 1rem;
font-family: inherit;
color: var(--ifm-font-color-base);
background: var(--ifm-background-surface-color);
outline: none;
transition: border-color 0.3s;
resize: none;
min-height: 3.25rem;
max-height: 8rem;
field-sizing: content;
}
.Input__field:focus {
border-color: #667eea;
}
.Input__field:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.Input__send {
display: flex;
justify-content: center;
align-items: center;
width: 3.25rem;
height: 3.25rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 50%;
color: white;
font-size: 1.125rem;
cursor: pointer;
flex-shrink: 0;
transition: transform 0.3s;
}
.Input__send:hover:not(:disabled) {
transform: scale(1.1);
}
.Input__send:disabled {
opacity: 0.5;
cursor: not-allowed;
}
-55
View File
@@ -1,55 +0,0 @@
import block from 'bem-css-modules';
import React, { ReactNode, useState } from 'react';
import { PaperPlaneIcon } from './icons';
import styles from './Input.module.css';
const b = block(styles, 'Input');
interface InputProps {
disabled?: boolean;
onSend?: (text: string) => void;
}
export function Input({ disabled, onSend }: InputProps): ReactNode {
const [input, setInput] = useState('');
const handleSend = () => {
const text = input.trim();
if (!text || disabled) {
return;
}
setInput('');
onSend?.(text);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className={b()}>
<textarea
className={b('field')}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message here..."
rows={1}
disabled={disabled}
/>
<button
className={b('send')}
onClick={handleSend}
disabled={disabled || !input.trim()}
>
<PaperPlaneIcon />
</button>
</div>
);
}
@@ -1,70 +0,0 @@
.Message {
max-width: 90%;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.Message_role_assistant {
align-self: flex-start;
}
.Message_role_user {
align-self: flex-end;
}
.Message__content {
padding: 0.75rem 1rem;
border-radius: 1.25rem;
line-height: 1.5;
}
.Message_role_assistant .Message__content {
background: var(--ifm-color-emphasis-100);
color: var(--ifm-font-color-base);
border-top-left-radius: 0.25rem;
}
.Message_role_user .Message__content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-bottom-right-radius: 0.25rem;
}
.Message__sources {
margin-top: 8px;
padding: 8px 1rem 0;
border-top: 1px solid var(--ifm-color-emphasis-200);
}
.Message__sourcesLabel {
margin-bottom: 4px;
color: var(--ifm-color-emphasis-600);
font-weight: var(--ifm-font-weight-semibold);
font-size: 0.75rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.Message__sourceLink {
display: block;
overflow: hidden;
color: var(--ifm-color-primary);
font-size: 0.8rem;
white-space: nowrap;
text-decoration: none;
text-overflow: ellipsis;
}
.Message__sourceLink:hover {
text-decoration: underline;
}
@media (max-width: 576px) {
.Message {
max-width: 100%;
}
}
@@ -1,42 +0,0 @@
import Link from '@docusaurus/Link';
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import { MD } from '@docuservix/entities/markdown';
import { IChatSource, sourceToPath, sourceToUrl } from '@docuservix/models/chat';
import styles from './Message.module.css';
const b = block(styles, 'Message');
interface MessageProps {
role: 'user' | 'assistant';
content: string;
sources?: IChatSource[];
}
export function Message({ role, content, sources }: MessageProps): ReactNode {
return (
<div className={b({ role })}>
<div className={b('content')}>
<MD>{content}</MD>
</div>
{sources && sources.length > 0 && (
<div className={b('sources')}>
<div className={b('sourcesLabel')}>Источники:</div>
{sources.map((src, j) => (
<Link
key={j}
to={sourceToUrl(src.file, src.anchor)}
className={b('sourceLink')}
>
{src.heading}
</Link>
))}
</div>
)}
</div>
);
}
@@ -1,65 +0,0 @@
.Messages {
flex: 1;
overflow-y: auto;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Typing indicator */
.Messages__typing {
display: flex;
align-items: center;
align-self: flex-start;
}
.Messages__typingIndicator {
display: flex;
gap: 0.25rem;
padding: 0.75rem 1rem;
background: var(--ifm-color-emphasis-100);
border-radius: 1.25rem;
border-top-left-radius: 0.25rem;
}
.Messages__typingIndicator span {
width: 0.5rem;
height: 0.5rem;
background: var(--ifm-color-emphasis-500);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.Messages__typingIndicator span:nth-child(1) { animation-delay: -0.32s; }
.Messages__typingIndicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Scrollbar */
.Messages::-webkit-scrollbar {
width: 6px;
}
.Messages::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
.Messages::-webkit-scrollbar-thumb {
background: var(--ifm-color-emphasis-300);
border-radius: 3px;
}
.Messages::-webkit-scrollbar-thumb:hover {
background: var(--ifm-color-emphasis-400);
}
@media (min-width: 576px) {
.Messages {
padding: 1.5rem;
}
}
@@ -1,39 +0,0 @@
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import { IChatMessage } from '@docuservix/models/chat';
import { Message } from './Message';
import styles from './Messages.module.css';
const b = block(styles, 'Messages');
interface MessagesProps {
messages: IChatMessage[];
typing?: boolean;
}
export function Messages({ messages, typing }: MessagesProps): ReactNode {
return (
<div className={b()}>
{messages.map((msg, i) => (
<Message
key={i}
role={msg.role}
content={msg.content}
sources={msg.sources}
/>
))}
{typing && (
<div className={b('typing')}>
<div className={b('typingIndicator')}>
<span></span>
<span></span>
<span></span>
</div>
</div>
)}
</div>
);
}
@@ -1,53 +0,0 @@
.Status {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0;
background: none;
border: none;
cursor: pointer;
color: var(--ifm-color-emphasis-600);
font-size: 0.85rem;
line-height: 1;
}
.Status:hover .Status__label {
color: var(--ifm-color-emphasis-800);
}
.Status:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
border-radius: 4px;
}
.Status__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ifm-color-emphasis-400);
flex-shrink: 0;
}
.Status_kind_server .Status__dot {
background: #22c55e;
}
.Status_kind_offline .Status__dot {
background: #ef4444;
}
.Status_kind_connecting .Status__dot {
background: #eab308;
animation: Status__pulse 1s ease-in-out infinite;
}
@keyframes Status__pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
@@ -1,38 +0,0 @@
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import { useChatStatus } from '@docuservix/hooks/useChatStatus';
import { ChatStatus } from '@docuservix/models/chat';
import styles from './Status.module.css';
const b = block(styles, 'Status');
function getLabel(status: ChatStatus): string {
switch (status.kind) {
case 'connecting':
return 'Подключение…';
case 'offline':
return 'Offline';
case 'server':
return status.label;
}
}
export function Status(): ReactNode {
const { status, apiUrl, recheck } = useChatStatus();
const label = getLabel(status);
return (
<button
type="button"
className={b({ kind: status.kind })}
onClick={recheck}
title={apiUrl}
aria-label={`Статус: ${label}. Нажмите для проверки`}
>
<span className={b('dot')} />
<span className={b('label')}>{label}</span>
</button>
);
}
-30
View File
@@ -1,30 +0,0 @@
import React, { ReactNode } from 'react';
export function RobotIcon(): ReactNode {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
);
}
export function PaperPlaneIcon(): ReactNode {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471z" />
</svg>
);
}
-1
View File
@@ -1 +0,0 @@
export { Chat } from './Chat';
+14 -26
View File
@@ -1,5 +1,3 @@
/* eslint-disable no-console -- logs required */
import fs from 'fs';
import path from 'path';
@@ -11,33 +9,23 @@ pinIndexToTop();
* Гарантирует наличие sidebar_position: 0 в front matter файла index.md
*/
function pinIndexToTop() {
const indexPath = path.join(docsDir, 'index.md');
const indexPath = path.join(docsDir, 'index.md');
if (!fs.existsSync(indexPath)) return;
if (!fs.existsSync(indexPath)) {
return;
}
let content = fs.readFileSync(indexPath, 'utf8');
let content = fs.readFileSync(indexPath, 'utf8');
if (content.startsWith('---\n')) {
const endIdx = content.indexOf('\n---\n', 4);
if (endIdx === -1) return;
if (content.startsWith('---\n')) {
const endIdx = content.indexOf('\n---\n', 4);
const frontMatter = content.slice(4, endIdx);
if (/^sidebar_position\s*:/m.test(frontMatter)) return;
if (endIdx === -1) {
return;
}
content = '---\nsidebar_position: 0\n' + frontMatter + '\n---\n' + content.slice(endIdx + 5);
} else {
content = '---\nsidebar_position: 0\n---\n' + content;
}
const frontMatter = content.slice(4, endIdx);
if (/^sidebar_position\s*:/m.test(frontMatter)) {
return;
}
content =
'---\nsidebar_position: 0\n' + frontMatter + '\n---\n' + content.slice(endIdx + 5);
} else {
content = '---\nsidebar_position: 0\n---\n' + content;
}
fs.writeFileSync(indexPath, content);
console.log('prepare-docs: pinned index.md to sidebar top');
fs.writeFileSync(indexPath, content);
console.log('prepare-docs: pinned index.md to sidebar top');
}
+9 -30
View File
@@ -17,35 +17,14 @@
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* Nord dark theme — https://www.nordtheme.com */
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
/* Frost — основной акцент (nord8) */
--ifm-color-primary: #88c0d0;
--ifm-color-primary-dark: #76b5c7;
--ifm-color-primary-darker: #6dafc3;
--ifm-color-primary-darkest: #5e81ac;
--ifm-color-primary-light: #95c8d8;
--ifm-color-primary-lighter: #9ecfdd;
--ifm-color-primary-lightest: #b4dce8;
/* Polar Night — фоны */
--ifm-background-color: #2e3440;
--ifm-background-surface-color: #3b4252;
--ifm-navbar-background-color: #2e3440;
--ifm-footer-background-color: #2e3440;
/* Snow Storm — текст */
--ifm-font-color-base: #eceff4;
--ifm-font-color-secondary: #d8dee9;
/* Emphasis scale: Polar Night → Snow Storm */
--ifm-color-emphasis-100: #3b4252;
--ifm-color-emphasis-200: #434c5e;
--ifm-color-emphasis-300: #4c566a;
--ifm-color-emphasis-400: #7b8ba0;
--ifm-color-emphasis-500: #d8dee9;
--ifm-color-emphasis-600: #e5e9f0;
--ifm-color-emphasis-700: #eceff4;
--docusaurus-highlighted-code-line-bg: rgba(136, 192, 208, 0.15);
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
+29 -36
View File
@@ -1,47 +1,40 @@
import type {ReactNode} from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Heading from '@theme/Heading';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading
as="h1"
className="hero__title"
>
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs"
>
Документация
</Link>
</div>
</div>
</header>
);
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs">
Документация
</Link>
</div>
</div>
</header>
);
}
export default function Home(): ReactNode {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
</Layout>
);
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
<HomepageHeader />
</Layout>
);
}
-82
View File
@@ -1,82 +0,0 @@
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common';
import { useColorMode } from '@docusaurus/theme-common';
import {
MermaidContainerClassName,
useMermaidConfig,
useMermaidRenderResult,
} from '@docusaurus/theme-mermaid/client';
import type { Props } from '@theme/Mermaid';
import type { RenderResult } from 'mermaid';
import React, { useRef, useEffect, useMemo, type ReactNode } from 'react';
import styles from './styles.module.css';
const NORD_THEME_VARIABLES = {
background: '#2e3440',
mainBkg: '#3b4252',
primaryColor: '#5e81ac',
primaryTextColor: '#eceff4',
primaryBorderColor: '#88c0d0',
secondaryColor: '#434c5e',
secondaryTextColor: '#d8dee9',
secondaryBorderColor: '#4c566a',
tertiaryColor: '#3b4252',
tertiaryTextColor: '#d8dee9',
tertiaryBorderColor: '#4c566a',
lineColor: '#88c0d0',
edgeLabelBackground: '#434c5e',
textColor: '#eceff4',
titleColor: '#88c0d0',
nodeBorder: '#4c566a',
clusterBkg: '#434c5e',
clusterBorder: '#4c566a',
};
function MermaidRenderResult({ renderResult }: { renderResult: RenderResult }): ReactNode {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
renderResult.bindFunctions?.(div);
}, [renderResult]);
return (
<div
ref={ref}
className={`${MermaidContainerClassName} ${styles.container}`}
// eslint-disable-next-line react/no-danger -- mermaid renders trusted SVG
dangerouslySetInnerHTML={{ __html: renderResult.svg }}
/>
);
}
function MermaidRenderer({ value }: Props): ReactNode {
const { colorMode } = useColorMode();
const baseConfig = useMermaidConfig();
const config = useMemo(
() =>
colorMode === 'dark'
? { ...baseConfig, themeVariables: NORD_THEME_VARIABLES }
: baseConfig,
[colorMode, baseConfig],
);
const renderResult = useMermaidRenderResult({ text: value, config });
if (renderResult === null) {
return null;
}
return <MermaidRenderResult renderResult={renderResult} />;
}
export default function Mermaid(props: Props): ReactNode {
return (
<ErrorBoundary fallback={(params) => <ErrorBoundaryErrorMessageFallback {...params} />}>
<MermaidRenderer {...props} />
</ErrorBoundary>
);
}
-7
View File
@@ -1,7 +0,0 @@
.container {
max-width: 100%;
}
.container > svg {
max-width: 100%;
}
+7 -15
View File
@@ -2,19 +2,11 @@
// It is here to improve your IDE experience (type-checking, autocompletion...),
// and can also run the package.json "typecheck" script manually.
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@docuservix/*": [
"plugins/docuservix/*"
]
},
"ignoreDeprecations": "6.0",
"strict": true
},
"exclude": [
".docusaurus",
"build"
]
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"ignoreDeprecations": "6.0",
"strict": true
},
"exclude": [".docusaurus", "build"]
}
+43 -2361
View File
File diff suppressed because it is too large Load Diff