diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2f77550 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# 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` diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 79ae98c..4e46379 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -6,6 +6,8 @@ import type { Config } from '@docusaurus/types'; import yaml from 'js-yaml'; import { themes as prismThemes } from 'prism-react-renderer'; +import docuservix from './plugins/docuservix'; + interface DocsConfig { title: string; project: { org: string; repo: string }; @@ -16,7 +18,7 @@ const docsConfig = yaml.load(fs.readFileSync('./.docuservix.yml', 'utf8')) as Do const { title } = docsConfig; -const url = process.env.DOCUSERVIX_URL; +const url = process.env.DOCUSERVIX_URL || 'http://example.com'; const { org, repo } = docsConfig.project; @@ -33,6 +35,7 @@ const config: Config = { markdown: { mermaid: true, }, + plugins: [docuservix()], themes: ['@docusaurus/theme-mermaid'], // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future diff --git a/package.json b/package.json index 7709f6f..ce868b3 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,14 @@ "@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-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.10.1", diff --git a/plugins/docuservix/entities/markdown/MD.module.css b/plugins/docuservix/entities/markdown/MD.module.css new file mode 100644 index 0000000..94c3868 --- /dev/null +++ b/plugins/docuservix/entities/markdown/MD.module.css @@ -0,0 +1,59 @@ +.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); +} diff --git a/plugins/docuservix/entities/markdown/MD.tsx b/plugins/docuservix/entities/markdown/MD.tsx new file mode 100644 index 0000000..add0a1d --- /dev/null +++ b/plugins/docuservix/entities/markdown/MD.tsx @@ -0,0 +1,17 @@ +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 ( +
+ {children} +
+ ); +} diff --git a/plugins/docuservix/entities/markdown/index.ts b/plugins/docuservix/entities/markdown/index.ts new file mode 100644 index 0000000..3288ffa --- /dev/null +++ b/plugins/docuservix/entities/markdown/index.ts @@ -0,0 +1 @@ +export { MD } from './MD'; diff --git a/plugins/docuservix/index.ts b/plugins/docuservix/index.ts new file mode 100644 index 0000000..50dbf9c --- /dev/null +++ b/plugins/docuservix/index.ts @@ -0,0 +1,31 @@ +import path from 'path'; + +import type { LoadContext, Plugin } from '@docusaurus/types'; + +export default function docuservix() { + return function pluginDocuservix(_context: LoadContext): Plugin { + return { + name: 'docuservix', + + configureWebpack() { + return { + resolve: { + alias: { + '@docuservix': path.resolve(__dirname), + }, + }, + }; + }, + + async contentLoaded({ actions }) { + const { addRoute } = actions; + + addRoute({ + path: '/chat', + component: '@docuservix/pages/chat', + exact: true, + }); + }, + }; + }; +} diff --git a/plugins/docuservix/models/chat.ts b/plugins/docuservix/models/chat.ts new file mode 100644 index 0000000..bdc8330 --- /dev/null +++ b/plugins/docuservix/models/chat.ts @@ -0,0 +1,8 @@ +export interface IChat { + messages: IChatMessage[]; +} + +export interface IChatMessage { + role: 'user' | 'assistant'; + content: string; +} diff --git a/plugins/docuservix/pages/chat/ChatPage.tsx b/plugins/docuservix/pages/chat/ChatPage.tsx new file mode 100644 index 0000000..8783705 --- /dev/null +++ b/plugins/docuservix/pages/chat/ChatPage.tsx @@ -0,0 +1,32 @@ +import Layout from '@theme/Layout'; +import { ReactNode } from 'react'; + +import { IChat } from '@docuservix/models/chat'; +import { Chat } from '@docuservix/widgets/chat'; + +const dialog: IChat = { + messages: [ + { + role: 'user', + content: 'Can you show me some CSS animations? It can be simple tools like chatbots...', + }, + { + role: 'assistant', + content: "Hello! I'm your **AI assistant**. How can I help you today?", + }, + ], +}; + +export function ChatPage(): ReactNode { + return ( + +
+ +
+
+ ); +} diff --git a/plugins/docuservix/pages/chat/index.ts b/plugins/docuservix/pages/chat/index.ts new file mode 100644 index 0000000..74f3d81 --- /dev/null +++ b/plugins/docuservix/pages/chat/index.ts @@ -0,0 +1,3 @@ +import { ChatPage } from './ChatPage'; + +export default ChatPage; diff --git a/plugins/docuservix/widgets/chat/Chat.module.css b/plugins/docuservix/widgets/chat/Chat.module.css new file mode 100644 index 0000000..1ff1edd --- /dev/null +++ b/plugins/docuservix/widgets/chat/Chat.module.css @@ -0,0 +1,24 @@ +.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; +} diff --git a/plugins/docuservix/widgets/chat/Chat.tsx b/plugins/docuservix/widgets/chat/Chat.tsx new file mode 100644 index 0000000..01d377d --- /dev/null +++ b/plugins/docuservix/widgets/chat/Chat.tsx @@ -0,0 +1,37 @@ +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 ( +
+
+ + {statusMessage &&
{statusMessage}
} + +
+ ); +} diff --git a/plugins/docuservix/widgets/chat/Header.module.css b/plugins/docuservix/widgets/chat/Header.module.css new file mode 100644 index 0000000..e48bbde --- /dev/null +++ b/plugins/docuservix/widgets/chat/Header.module.css @@ -0,0 +1,32 @@ +.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; +} + +.Header__info p { + margin: 0; + color: var(--ifm-color-emphasis-600); + font-size: 0.85rem; +} diff --git a/plugins/docuservix/widgets/chat/Header.tsx b/plugins/docuservix/widgets/chat/Header.tsx new file mode 100644 index 0000000..b438758 --- /dev/null +++ b/plugins/docuservix/widgets/chat/Header.tsx @@ -0,0 +1,21 @@ +import block from 'bem-css-modules'; +import React, { ReactNode } from 'react'; + +import styles from './Header.module.css'; +import { RobotIcon } from './icons'; + +const b = block(styles, 'Header'); + +export function Header(): ReactNode { + return ( +
+
+ +
+
+

AI Assistant

+

Ready to help

+
+
+ ); +} diff --git a/plugins/docuservix/widgets/chat/Input.module.css b/plugins/docuservix/widgets/chat/Input.module.css new file mode 100644 index 0000000..2ab1f43 --- /dev/null +++ b/plugins/docuservix/widgets/chat/Input.module.css @@ -0,0 +1,58 @@ +.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; +} diff --git a/plugins/docuservix/widgets/chat/Input.tsx b/plugins/docuservix/widgets/chat/Input.tsx new file mode 100644 index 0000000..d61777d --- /dev/null +++ b/plugins/docuservix/widgets/chat/Input.tsx @@ -0,0 +1,55 @@ +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) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( +
+