Compare commits

2 Commits
Author SHA1 Message Date
arswarog 2690754abd chat: добавление чата
Reviewed-on: #7
Co-authored-by: Arswarog <arswarog@yandex.ru>
2026-08-08 15:59:25 +03:00
arswarog ec5d2f14f4 feat(theme): тёмная тема на палитре Nord
## Зачем

Более комфортно для глаз

## Реализация

**Почему не хватило конфига в themeConfig.mermaid.options**

Docusaurus инициализирует Mermaid так:

```ts
mermaid.initialize({
  ...options,          // статично, оба режима
  theme: isDark ? 'base' : 'default',
});
```

`themeVariables` лежат внутри `options` и применяются независимо от `colorMode`. Попытка передать Nord-цвета через `options` дала бы Nord-оттенки и в светлом режиме.

**Почему swizzle, а не CSS-переопределения**

Mermaid рендерит SVG и встраивает финальные hex-значения непосредственно в `<style>` внутри SVG-элемента. CSS снаружи SVG не пробивает эти стили без `!important` на каждый конкретный селектор — хрупко и нестабильно при обновлениях Mermaid.Reviewed-on: #9

Co-authored-by: Arswarog <arswarog@yandex.ru>
2026-08-08 10:37:30 +03:00
9 changed files with 196 additions and 30 deletions
+67
View File
@@ -1,3 +1,70 @@
# Добро пожаловать в 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;
}
}
```
+6
View File
@@ -127,6 +127,12 @@ const config: Config = {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
mermaid: {
theme: {
light: 'default',
dark: 'base',
},
},
} satisfies Preset.ThemeConfig,
};
+1 -1
View File
@@ -19,7 +19,7 @@ function useQuery(): string {
}
export function useChat(): UseChatResult {
const chatEndpoint = useOptions().api + '/chat';
const chatEndpoint = useOptions().api + '/v1/chat';
const urlQuery = useQuery();
const [messages, setMessages] = useState<IChatMessage[]>([]);
+1 -1
View File
@@ -5,7 +5,7 @@ import type { LoadContext, Plugin } from '@docusaurus/types';
import { DocuservixOptions } from '@docuservix/models/docuservix';
export default function docuservix(options: Partial<DocuservixOptions> = {}) {
const { api = '/api' } = options;
const api = process.env.DOCUSERVIX_API || options.api || '/api';
return function pluginDocuservix(_context: LoadContext): Plugin {
return {
+1 -18
View File
@@ -15,23 +15,6 @@ export interface IChatMessage {
sources?: IChatSource[];
}
function stripNumericPrefixes(p: string): string {
return p
.split('/')
.map((seg) => seg.replace(/^\d+-/, ''))
.join('/');
}
export function sourceToUrl(file: string, anchor: string): string {
let p = file.replace(/^docs\//, '').replace(/\.md$/, '');
p = stripNumericPrefixes(p);
return `/docs/${p}${anchor ? `#${anchor}` : ''}`;
}
export function sourceToPath(file: string): string {
const p = file.replace(/^docs\//, '').replace(/\.md$/, '');
return stripNumericPrefixes(p);
return `/${file}${anchor ? `#${anchor}` : ''}`;
}
+1 -1
View File
@@ -32,7 +32,7 @@ export function Message({ role, content, sources }: MessageProps): ReactNode {
to={sourceToUrl(src.file, src.anchor)}
className={b('sourceLink')}
>
{src.heading || sourceToPath(src.file)}
{src.heading}
</Link>
))}
</div>
+30 -9
View File
@@ -17,14 +17,35 @@
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
/* Nord dark theme — https://www.nordtheme.com */
[data-theme='dark'] {
--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);
/* 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);
}
+82
View File
@@ -0,0 +1,82 @@
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
@@ -0,0 +1,7 @@
.container {
max-width: 100%;
}
.container > svg {
max-width: 100%;
}