Compare commits

7 Commits
23 changed files with 54 additions and 537 deletions
+5 -7
View File
@@ -11,13 +11,11 @@ Docuservix docs — шаблон документационного сайта
## Commands ## Commands
Используется **yarn**. - `npm start` — dev-сервер
- `npm run build` — production-сборка в `build/`
- `yarn start` — dev-сервер - `npm run typecheck` — проверка типов (tsc)
- `yarn build` — production-сборка в `build/` - `npm run prettier:check` — проверка форматирования
- `yarn typecheck` — проверка типов (tsc) - `npm run prettier:fix` — автоформатирование
- `yarn prettier:check` — проверка форматирования
- `yarn prettier:fix` — автоформатирование
## Architecture ## Architecture
-67
View File
@@ -1,70 +1,3 @@
# Добро пожаловать в Docuservix! # Добро пожаловать в Docuservix!
Вам надо настроить публикацию документации по инструкции в https://git.jt4d.ru/jt4d/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,12 +127,6 @@ const config: Config = {
theme: prismThemes.github, theme: prismThemes.github,
darkTheme: prismThemes.dracula, darkTheme: prismThemes.dracula,
}, },
mermaid: {
theme: {
light: 'default',
dark: 'base',
},
},
} satisfies Preset.ThemeConfig, } satisfies Preset.ThemeConfig,
}; };
+1 -3
View File
@@ -48,9 +48,7 @@
"js-yaml": "^4.2.0", "js-yaml": "^4.2.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0"
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1",
@@ -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';
+1 -1
View File
@@ -19,7 +19,7 @@ function useQuery(): string {
} }
export function useChat(): UseChatResult { export function useChat(): UseChatResult {
const chatEndpoint = useOptions().api + '/v1/chat'; const chatEndpoint = useOptions().api + '/chat';
const urlQuery = useQuery(); const urlQuery = useQuery();
const [messages, setMessages] = useState<IChatMessage[]>([]); const [messages, setMessages] = useState<IChatMessage[]>([]);
-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 };
}
+1 -1
View File
@@ -5,7 +5,7 @@ import type { LoadContext, Plugin } from '@docusaurus/types';
import { DocuservixOptions } from '@docuservix/models/docuservix'; import { DocuservixOptions } from '@docuservix/models/docuservix';
export default function docuservix(options: Partial<DocuservixOptions> = {}) { export default function docuservix(options: Partial<DocuservixOptions> = {}) {
const api = process.env.DOCUSERVIX_API || options.api || '/api'; const { api = '/api' } = options;
return function pluginDocuservix(_context: LoadContext): Plugin { return function pluginDocuservix(_context: LoadContext): Plugin {
return { return {
+18 -6
View File
@@ -1,8 +1,3 @@
export type ChatStatus =
| { kind: 'connecting' }
| { kind: 'offline' }
| { kind: 'server'; label: string };
export interface IChat { export interface IChat {
messages: IChatMessage[]; messages: IChatMessage[];
} }
@@ -20,6 +15,23 @@ export interface IChatMessage {
sources?: IChatSource[]; sources?: IChatSource[];
} }
function stripNumericPrefixes(p: string): string {
return p
.split('/')
.map((seg) => seg.replace(/^\d+-/, ''))
.join('/');
}
export function sourceToUrl(file: string, anchor: string): string { export function sourceToUrl(file: string, anchor: string): string {
return `/${file}${anchor ? `#${anchor}` : ''}`; 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);
} }
+1 -1
View File
@@ -29,7 +29,7 @@ export function Chat({ dialog, typing, statusMessage, onSend }: ChatProps): Reac
/> />
{statusMessage && <div className={b('statusMessage')}>{statusMessage}</div>} {statusMessage && <div className={b('statusMessage')}>{statusMessage}</div>}
<Input <Input
disabled={typing} loading={typing}
onSend={onSend} onSend={onSend}
/> />
</div> </div>
@@ -24,3 +24,9 @@
color: var(--ifm-font-color-base); color: var(--ifm-font-color-base);
font-size: 1.125rem; font-size: 1.125rem;
} }
.Header__info p {
margin: 0;
color: var(--ifm-color-emphasis-600);
font-size: 0.85rem;
}
+1 -2
View File
@@ -3,7 +3,6 @@ import React, { ReactNode } from 'react';
import styles from './Header.module.css'; import styles from './Header.module.css';
import { RobotIcon } from './icons'; import { RobotIcon } from './icons';
import { Status } from './Status';
const b = block(styles, 'Header'); const b = block(styles, 'Header');
@@ -15,7 +14,7 @@ export function Header(): ReactNode {
</div> </div>
<div className={b('info')}> <div className={b('info')}>
<h3>AI Assistant</h3> <h3>AI Assistant</h3>
<Status /> <p>Ready to help</p>
</div> </div>
</div> </div>
); );
+5 -5
View File
@@ -7,17 +7,17 @@ import styles from './Input.module.css';
const b = block(styles, 'Input'); const b = block(styles, 'Input');
interface InputProps { interface InputProps {
disabled?: boolean; loading?: boolean;
onSend?: (text: string) => void; onSend?: (text: string) => void;
} }
export function Input({ disabled, onSend }: InputProps): ReactNode { export function Input({ loading, onSend }: InputProps): ReactNode {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const handleSend = () => { const handleSend = () => {
const text = input.trim(); const text = input.trim();
if (!text || disabled) { if (!text || loading) {
return; return;
} }
@@ -41,12 +41,12 @@ export function Input({ disabled, onSend }: InputProps): ReactNode {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Type your message here..." placeholder="Type your message here..."
rows={1} rows={1}
disabled={disabled} disabled={loading}
/> />
<button <button
className={b('send')} className={b('send')}
onClick={handleSend} onClick={handleSend}
disabled={disabled || !input.trim()} disabled={loading || !input.trim()}
> >
<PaperPlaneIcon /> <PaperPlaneIcon />
</button> </button>
@@ -1,5 +1,5 @@
.Message { .Message {
max-width: 90%; max-width: 85%;
animation: slideIn 0.3s ease-out; animation: slideIn 0.3s ease-out;
} }
@@ -63,8 +63,8 @@
text-decoration: underline; text-decoration: underline;
} }
@media (max-width: 576px) { @media (min-width: 576px) {
.Message { .Message {
max-width: 100%; max-width: 75%;
} }
} }
+2 -6
View File
@@ -2,8 +2,6 @@ import Link from '@docusaurus/Link';
import block from 'bem-css-modules'; import block from 'bem-css-modules';
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { MD } from '@docuservix/entities/markdown';
import { IChatSource, sourceToPath, sourceToUrl } from '@docuservix/models/chat'; import { IChatSource, sourceToPath, sourceToUrl } from '@docuservix/models/chat';
import styles from './Message.module.css'; import styles from './Message.module.css';
@@ -19,9 +17,7 @@ interface MessageProps {
export function Message({ role, content, sources }: MessageProps): ReactNode { export function Message({ role, content, sources }: MessageProps): ReactNode {
return ( return (
<div className={b({ role })}> <div className={b({ role })}>
<div className={b('content')}> <div className={b('content')}>{content}</div>
<MD>{content}</MD>
</div>
{sources && sources.length > 0 && ( {sources && sources.length > 0 && (
<div className={b('sources')}> <div className={b('sources')}>
@@ -32,7 +28,7 @@ export function Message({ role, content, sources }: MessageProps): ReactNode {
to={sourceToUrl(src.file, src.anchor)} to={sourceToUrl(src.file, src.anchor)}
className={b('sourceLink')} className={b('sourceLink')}
> >
{src.heading} {src.heading || sourceToPath(src.file)}
</Link> </Link>
))} ))}
</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>
);
}
+9 -30
View File
@@ -17,35 +17,14 @@
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); --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'] { [data-theme='dark'] {
/* Frost — основной акцент (nord8) */ --ifm-color-primary: #25c2a0;
--ifm-color-primary: #88c0d0; --ifm-color-primary-dark: #21af90;
--ifm-color-primary-dark: #76b5c7; --ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darker: #6dafc3; --ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-darkest: #5e81ac; --ifm-color-primary-light: #29d5b0;
--ifm-color-primary-light: #95c8d8; --ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lighter: #9ecfdd; --ifm-color-primary-lightest: #4fddbf;
--ifm-color-primary-lightest: #b4dce8; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
/* 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
@@ -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%;
}
+1 -23
View File
@@ -7308,11 +7308,6 @@ html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html-url-attributes@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87"
integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==
html-void-elements@^3.0.0: html-void-elements@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
@@ -10561,23 +10556,6 @@ react-loadable-ssr-addon-v5-slorber@^1.0.3:
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
react-markdown@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca"
integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
devlop "^1.0.0"
hast-util-to-jsx-runtime "^2.0.0"
html-url-attributes "^3.0.0"
mdast-util-to-hast "^13.0.0"
remark-parse "^11.0.0"
remark-rehype "^11.0.0"
unified "^11.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
react-router-config@^5.1.1: react-router-config@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
@@ -10831,7 +10809,7 @@ remark-frontmatter@^5.0.0:
micromark-extension-frontmatter "^2.0.0" micromark-extension-frontmatter "^2.0.0"
unified "^11.0.0" unified "^11.0.0"
remark-gfm@^4.0.0, remark-gfm@^4.0.1: remark-gfm@^4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b" resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b"
integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==