From e38bd068bacdc09800231ee0f47d81f702123c7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9C=A7=E9=9B=A8=E3=83=90=E3=83=8B=E3=83=A9?=
<118162831+kirisamevanilla@users.noreply.github.com>
Date: Sun, 29 Mar 2026 01:07:03 +0800
Subject: [PATCH 1/2] wip: init
---
package.json | 3 +
pnpm-lock.yaml | 26 ++++
public/docs/en/_structure.json | 30 ++++
public/docs/en/api/renderer.md | 17 +++
public/docs/en/guide/quick-start.md | 26 ++++
public/docs/en/index.md | 25 +++
public/docs/ja/_structure.json | 30 ++++
public/docs/ja/api/renderer.md | 9 ++
public/docs/ja/guide/quick-start.md | 15 ++
public/docs/ja/index.md | 13 ++
public/docs/ko/_structure.json | 30 ++++
public/docs/ko/api/renderer.md | 9 ++
public/docs/ko/guide/quick-start.md | 15 ++
public/docs/ko/index.md | 9 ++
public/docs/zh/_structure.json | 30 ++++
public/docs/zh/api/renderer.md | 17 +++
public/docs/zh/guide/quick-start.md | 26 ++++
public/docs/zh/index.md | 25 +++
public/i18n/en.json | 11 +-
public/i18n/ja.json | 11 +-
public/i18n/ko.json | 11 +-
public/i18n/zh.json | 11 +-
src/App.tsx | 2 +
src/components/Docs/CodeBlock.tsx | 27 ++++
src/components/Docs/DocLayout.tsx | 19 +++
src/components/Docs/DocPagination.tsx | 48 ++++++
src/components/Docs/DocRenderer.tsx | 98 ++++++++++++
src/components/Docs/DocSearchBar.tsx | 47 ++++++
src/components/Docs/DocSidebar.tsx | 90 +++++++++++
src/components/Docs/DocTableOfContents.tsx | 72 +++++++++
src/components/Docs/index.ts | 6 +
src/components/UnifiedHeader/DesktopNav.tsx | 3 +
src/components/UnifiedHeader/MobileNav.tsx | 8 +-
src/config/docRoutes.ts | 47 ++++++
src/hooks/index.ts | 3 +-
src/hooks/useDocSearch.ts | 55 +++++++
src/index.css | 17 +++
src/pages/DocsPage.tsx | 160 ++++++++++++++++++++
src/types/doc.ts | 36 +++++
src/types/index.ts | 3 +-
src/utils/docSearch.ts | 65 ++++++++
src/utils/documentLoader.ts | 55 +++++++
vite.config.ts | 6 +
43 files changed, 1257 insertions(+), 9 deletions(-)
create mode 100644 public/docs/en/_structure.json
create mode 100644 public/docs/en/api/renderer.md
create mode 100644 public/docs/en/guide/quick-start.md
create mode 100644 public/docs/en/index.md
create mode 100644 public/docs/ja/_structure.json
create mode 100644 public/docs/ja/api/renderer.md
create mode 100644 public/docs/ja/guide/quick-start.md
create mode 100644 public/docs/ja/index.md
create mode 100644 public/docs/ko/_structure.json
create mode 100644 public/docs/ko/api/renderer.md
create mode 100644 public/docs/ko/guide/quick-start.md
create mode 100644 public/docs/ko/index.md
create mode 100644 public/docs/zh/_structure.json
create mode 100644 public/docs/zh/api/renderer.md
create mode 100644 public/docs/zh/guide/quick-start.md
create mode 100644 public/docs/zh/index.md
create mode 100644 src/components/Docs/CodeBlock.tsx
create mode 100644 src/components/Docs/DocLayout.tsx
create mode 100644 src/components/Docs/DocPagination.tsx
create mode 100644 src/components/Docs/DocRenderer.tsx
create mode 100644 src/components/Docs/DocSearchBar.tsx
create mode 100644 src/components/Docs/DocSidebar.tsx
create mode 100644 src/components/Docs/DocTableOfContents.tsx
create mode 100644 src/components/Docs/index.ts
create mode 100644 src/config/docRoutes.ts
create mode 100644 src/hooks/useDocSearch.ts
create mode 100644 src/pages/DocsPage.tsx
create mode 100644 src/types/doc.ts
create mode 100644 src/utils/docSearch.ts
create mode 100644 src/utils/documentLoader.ts
diff --git a/package.json b/package.json
index b2357df..2c019db 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,10 @@
"@tailwindcss/vite": "^4.2.1",
"axios": "^1.13.5",
"framer-motion": "^12.34.3",
+ "fuse.js": "^7.1.0",
"github-markdown-css": "^5.9.0",
+ "github-slugger": "^2.0.0",
+ "highlight.js": "^11.11.1",
"js-md5": "^0.8.3",
"jszip": "^3.10.1",
"react": "^19.2.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a9bc7ca..a0cb156 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,9 +20,18 @@ importers:
framer-motion:
specifier: ^12.34.3
version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ fuse.js:
+ specifier: ^7.1.0
+ version: 7.1.0
github-markdown-css:
specifier: ^5.9.0
version: 5.9.0
+ github-slugger:
+ specifier: ^2.0.0
+ version: 2.0.0
+ highlight.js:
+ specifier: ^11.11.1
+ version: 11.11.1
js-md5:
specifier: ^0.8.3
version: 0.8.3
@@ -1330,6 +1339,10 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ fuse.js@7.1.0:
+ resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
+ engines: {node: '>=10'}
+
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -1346,6 +1359,9 @@ packages:
resolution: {integrity: sha512-tmT5sY+zvg2302XLYEfH2mtkViIM1SWf2nvYoF5N1ZsO0V6B2qZTiw3GOzw4vpjLygK/KG35qRlPFweHqfzz5w==}
engines: {node: '>=10'}
+ github-slugger@2.0.0:
+ resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -1393,6 +1409,10 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ highlight.js@11.11.1:
+ resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
+ engines: {node: '>=12.0.0'}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@@ -3249,6 +3269,8 @@ snapshots:
function-bind@1.1.2: {}
+ fuse.js@7.1.0: {}
+
gensync@1.0.0-beta.2: {}
get-intrinsic@1.3.0:
@@ -3271,6 +3293,8 @@ snapshots:
github-markdown-css@5.9.0: {}
+ github-slugger@2.0.0: {}
+
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
@@ -3325,6 +3349,8 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
+ highlight.js@11.11.1: {}
+
html-url-attributes@3.0.1: {}
ignore@5.3.2: {}
diff --git a/public/docs/en/_structure.json b/public/docs/en/_structure.json
new file mode 100644
index 0000000..64d7070
--- /dev/null
+++ b/public/docs/en/_structure.json
@@ -0,0 +1,30 @@
+{
+ "title": "Documentation",
+ "items": [
+ {
+ "title": "Introduction",
+ "slug": "index",
+ "description": "Docs portal overview"
+ },
+ {
+ "title": "Guides",
+ "children": [
+ {
+ "title": "Quick Start",
+ "slug": "guide/quick-start",
+ "description": "Integrate docs pages quickly"
+ }
+ ]
+ },
+ {
+ "title": "API",
+ "children": [
+ {
+ "title": "Renderer",
+ "slug": "api/renderer",
+ "description": "Markdown rendering capabilities"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/docs/en/api/renderer.md b/public/docs/en/api/renderer.md
new file mode 100644
index 0000000..119e1d3
--- /dev/null
+++ b/public/docs/en/api/renderer.md
@@ -0,0 +1,17 @@
+# Renderer Capability
+
+The docs page renders markdown via `react-markdown` + `remark-gfm` + `github-markdown-css`.
+
+## Supported Features
+
+- Headings, lists, tables, blockquotes
+- Syntax-highlighted code blocks
+- Auto-generated TOC for current page
+
+## Extension Ideas
+
+You can extend this setup with:
+
+- Mermaid diagrams
+- Admonition blocks
+- Version switcher
diff --git a/public/docs/en/guide/quick-start.md b/public/docs/en/guide/quick-start.md
new file mode 100644
index 0000000..4f54ea8
--- /dev/null
+++ b/public/docs/en/guide/quick-start.md
@@ -0,0 +1,26 @@
+# Quick Start
+
+This page explains how to add a new documentation entry.
+
+## Step 1: Add a markdown file
+
+Create a file under a language directory:
+
+```text
+public/docs/en/guide/new-topic.md
+```
+
+## Step 2: Update navigation structure
+
+Add a node to `_structure.json`:
+
+```json
+{
+ "title": "New Topic",
+ "slug": "guide/new-topic"
+}
+```
+
+## Step 3: Verify route
+
+Open `/docs/guide/new-topic` and ensure content renders and appears in the sidebar.
diff --git a/public/docs/en/index.md b/public/docs/en/index.md
new file mode 100644
index 0000000..0a5853c
--- /dev/null
+++ b/public/docs/en/index.md
@@ -0,0 +1,25 @@
+# Majdata Docs
+
+Welcome to the Majdata documentation portal. This site is rendered with the existing github-markdown pipeline and includes:
+
+- Collapsible sidebar navigation
+- Right-side table of contents
+- Syntax-highlighted code blocks
+- Previous / Next navigation
+- Client-side document search
+
+## Suggested Structure
+
+Keep each topic in its own markdown file and maintain sidebar order in `_structure.json`.
+
+## Example Code Block
+
+```ts
+export function hello(name: string): string {
+ return `hello, ${name}`;
+}
+```
+
+## Next Step
+
+Continue with the Quick Start page to add your own docs content.
diff --git a/public/docs/ja/_structure.json b/public/docs/ja/_structure.json
new file mode 100644
index 0000000..dea33c2
--- /dev/null
+++ b/public/docs/ja/_structure.json
@@ -0,0 +1,30 @@
+{
+ "title": "ドキュメント",
+ "items": [
+ {
+ "title": "イントロダクション",
+ "slug": "index",
+ "description": "ドキュメント全体の概要"
+ },
+ {
+ "title": "ガイド",
+ "children": [
+ {
+ "title": "クイックスタート",
+ "slug": "guide/quick-start",
+ "description": "ドキュメントを素早く追加"
+ }
+ ]
+ },
+ {
+ "title": "API",
+ "children": [
+ {
+ "title": "レンダラー",
+ "slug": "api/renderer",
+ "description": "Markdown レンダリング能力"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/docs/ja/api/renderer.md b/public/docs/ja/api/renderer.md
new file mode 100644
index 0000000..8137121
--- /dev/null
+++ b/public/docs/ja/api/renderer.md
@@ -0,0 +1,9 @@
+# レンダラー能力
+
+`react-markdown` と `remark-gfm`、`github-markdown-css` を使って文書を表示します。
+
+## 対応内容
+
+- 基本的な markdown 要素
+- コードハイライト
+- 現在ページ TOC
diff --git a/public/docs/ja/guide/quick-start.md b/public/docs/ja/guide/quick-start.md
new file mode 100644
index 0000000..f65e36f
--- /dev/null
+++ b/public/docs/ja/guide/quick-start.md
@@ -0,0 +1,15 @@
+# クイックスタート
+
+新しいドキュメントページを追加する手順です。
+
+## ステップ1
+
+言語ディレクトリに markdown を追加します。
+
+## ステップ2
+
+`_structure.json` にナビ項目を追加します。
+
+## ステップ3
+
+`/docs/...` で表示できることを確認します。
diff --git a/public/docs/ja/index.md b/public/docs/ja/index.md
new file mode 100644
index 0000000..2b40565
--- /dev/null
+++ b/public/docs/ja/index.md
@@ -0,0 +1,13 @@
+# Majdata Docs
+
+Majdata ドキュメントへようこそ。既存の github-markdown レンダリングを利用し、以下を提供します。
+
+- 折りたたみ可能な左ナビ
+- 右側 TOC
+- コードブロックのシンタックスハイライト
+- 前後ページナビ
+- クライアントサイド検索
+
+## 構成の考え方
+
+トピックごとに markdown を分け、`_structure.json` で順序を管理します。
diff --git a/public/docs/ko/_structure.json b/public/docs/ko/_structure.json
new file mode 100644
index 0000000..9470311
--- /dev/null
+++ b/public/docs/ko/_structure.json
@@ -0,0 +1,30 @@
+{
+ "title": "문서",
+ "items": [
+ {
+ "title": "소개",
+ "slug": "index",
+ "description": "문서 포털 개요"
+ },
+ {
+ "title": "가이드",
+ "children": [
+ {
+ "title": "빠른 시작",
+ "slug": "guide/quick-start",
+ "description": "문서 페이지 빠르게 추가"
+ }
+ ]
+ },
+ {
+ "title": "API",
+ "children": [
+ {
+ "title": "렌더러",
+ "slug": "api/renderer",
+ "description": "Markdown 렌더링 기능"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/docs/ko/api/renderer.md b/public/docs/ko/api/renderer.md
new file mode 100644
index 0000000..711082f
--- /dev/null
+++ b/public/docs/ko/api/renderer.md
@@ -0,0 +1,9 @@
+# 렌더러 기능
+
+문서 페이지는 `react-markdown` + `remark-gfm` + `github-markdown-css` 조합으로 렌더링됩니다.
+
+## 지원 항목
+
+- 기본 markdown 요소
+- 코드 하이라이트
+- 현재 페이지 TOC 자동 생성
diff --git a/public/docs/ko/guide/quick-start.md b/public/docs/ko/guide/quick-start.md
new file mode 100644
index 0000000..e1735c3
--- /dev/null
+++ b/public/docs/ko/guide/quick-start.md
@@ -0,0 +1,15 @@
+# 빠른 시작
+
+새 문서를 추가하는 방법입니다.
+
+## 1단계
+
+언어 디렉토리에 markdown 파일을 추가하세요.
+
+## 2단계
+
+`_structure.json`에 항목을 추가하세요.
+
+## 3단계
+
+`/docs/...` 경로에서 정상 렌더링을 확인하세요.
diff --git a/public/docs/ko/index.md b/public/docs/ko/index.md
new file mode 100644
index 0000000..39350a9
--- /dev/null
+++ b/public/docs/ko/index.md
@@ -0,0 +1,9 @@
+# Majdata Docs
+
+Majdata 문서 포털에 오신 것을 환영합니다. 기존 github-markdown 렌더링 로직을 기반으로 다음 기능을 제공합니다.
+
+- 접을 수 있는 좌측 네비게이션
+- 우측 TOC
+- 코드 블록 문법 하이라이트
+- 이전 / 다음 페이지
+- 클라이언트 검색
diff --git a/public/docs/zh/_structure.json b/public/docs/zh/_structure.json
new file mode 100644
index 0000000..a301be9
--- /dev/null
+++ b/public/docs/zh/_structure.json
@@ -0,0 +1,30 @@
+{
+ "title": "文档",
+ "items": [
+ {
+ "title": "简介",
+ "slug": "index",
+ "description": "文档站总览"
+ },
+ {
+ "title": "指南",
+ "children": [
+ {
+ "title": "快速开始",
+ "slug": "guide/quick-start",
+ "description": "在项目中接入 Docs 站"
+ }
+ ]
+ },
+ {
+ "title": "API",
+ "children": [
+ {
+ "title": "渲染器能力",
+ "slug": "api/renderer",
+ "description": "Markdown 渲染与扩展点"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/docs/zh/api/renderer.md b/public/docs/zh/api/renderer.md
new file mode 100644
index 0000000..cac5d5a
--- /dev/null
+++ b/public/docs/zh/api/renderer.md
@@ -0,0 +1,17 @@
+# 渲染器能力
+
+Docs 页面使用 `react-markdown` + `remark-gfm` + `github-markdown-css` 进行渲染。
+
+## 已支持特性
+
+- 标题、列表、表格、引用
+- 代码块语法高亮
+- 自动生成当前页目录(TOC)
+
+## 扩展方向
+
+可以继续扩展:
+
+- mermaid 图渲染
+- admonition 提示块
+- 文档版本切换
diff --git a/public/docs/zh/guide/quick-start.md b/public/docs/zh/guide/quick-start.md
new file mode 100644
index 0000000..a1016c5
--- /dev/null
+++ b/public/docs/zh/guide/quick-start.md
@@ -0,0 +1,26 @@
+# 快速开始
+
+本页说明如何在现有项目中新增文档条目。
+
+## 第一步:添加 markdown 文件
+
+在语言目录下新增文件,例如:
+
+```text
+public/docs/zh/guide/new-topic.md
+```
+
+## 第二步:更新结构文件
+
+在 `_structure.json` 里添加一个节点:
+
+```json
+{
+ "title": "新主题",
+ "slug": "guide/new-topic"
+}
+```
+
+## 第三步:验证路由
+
+访问 `/docs/guide/new-topic`,确认内容可以渲染并出现在侧栏中。
diff --git a/public/docs/zh/index.md b/public/docs/zh/index.md
new file mode 100644
index 0000000..9e74b5b
--- /dev/null
+++ b/public/docs/zh/index.md
@@ -0,0 +1,25 @@
+# Majdata Docs
+
+欢迎来到 Majdata 文档站。这里使用当前项目中的 github-markdown 渲染能力,并提供:
+
+- 左侧可折叠导航
+- 右侧当前页 TOC
+- 代码块高亮
+- 上一页 / 下一页
+- 客户端文档搜索
+
+## 目录结构建议
+
+建议将文档按照功能模块拆分为独立 markdown 文件,并在 `_structure.json` 中维护导航树顺序。
+
+## 一个示例代码块
+
+```ts
+export function hello(name: string): string {
+ return `hello, ${name}`;
+}
+```
+
+## 下一步
+
+你可以继续阅读快速开始章节,了解如何新增文档页面。
diff --git a/public/i18n/en.json b/public/i18n/en.json
index cde01c1..8a8f32d 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -317,5 +317,14 @@
"notfound.description": "Sorry, the page you are looking for does not exist or has been removed.",
"notfound.redirect": "Redirecting to home page in ${countdown} seconds...",
"notfound.backHome": "Back to Home",
- "notfound.goBack": "Go Back"
+ "notfound.goBack": "Go Back",
+ "DocsTitle": "Docs",
+ "DocsSearchPlaceholder": "Search docs...",
+ "DocsOnThisPage": "On this page",
+ "DocsNoResults": "No matching documents",
+ "DocsLoading": "Building search index...",
+ "DocsNotFound": "Document not found",
+ "DocsNotFoundDesc": "The requested document does not exist or is not translated yet.",
+ "DocsPrev": "Previous",
+ "DocsNext": "Next"
}
\ No newline at end of file
diff --git a/public/i18n/ja.json b/public/i18n/ja.json
index fa1ce78..94aea31 100644
--- a/public/i18n/ja.json
+++ b/public/i18n/ja.json
@@ -317,5 +317,14 @@
"notfound.description": "申し訳ございません。お探しのページは存在しないか、削除された可能性があります。",
"notfound.redirect": "${countdown}秒後にホームページに戻ります...",
"notfound.backHome": "ホームに戻る",
- "notfound.goBack": "前のページに戻る"
+ "notfound.goBack": "前のページに戻る",
+ "DocsTitle": "ドキュメント",
+ "DocsSearchPlaceholder": "ドキュメントを検索...",
+ "DocsOnThisPage": "このページ",
+ "DocsNoResults": "一致するドキュメントがありません",
+ "DocsLoading": "検索インデックスを構築中...",
+ "DocsNotFound": "ドキュメントが見つかりません",
+ "DocsNotFoundDesc": "要求されたドキュメントが存在しないか、まだ翻訳されていません。",
+ "DocsPrev": "前へ",
+ "DocsNext": "次へ"
}
\ No newline at end of file
diff --git a/public/i18n/ko.json b/public/i18n/ko.json
index 38277a0..847821f 100644
--- a/public/i18n/ko.json
+++ b/public/i18n/ko.json
@@ -317,5 +317,14 @@
"notfound.description": "죄송합니다. 요청하신 페이지가 존재하지 않거나 삭제되었습니다.",
"notfound.redirect": "${countdown}초 후 홈페이지로 돌아갑니다...",
"notfound.backHome": "홈으로 돌아가기",
- "notfound.goBack": "이전 페이지로"
+ "notfound.goBack": "이전 페이지로",
+ "DocsTitle": "문서",
+ "DocsSearchPlaceholder": "문서 검색...",
+ "DocsOnThisPage": "이 페이지",
+ "DocsNoResults": "일치하는 문서가 없습니다",
+ "DocsLoading": "검색 인덱스를 준비하는 중...",
+ "DocsNotFound": "문서를 찾을 수 없습니다",
+ "DocsNotFoundDesc": "요청한 문서가 없거나 아직 번역되지 않았습니다.",
+ "DocsPrev": "이전",
+ "DocsNext": "다음"
}
\ No newline at end of file
diff --git a/public/i18n/zh.json b/public/i18n/zh.json
index eca33d9..2e4be60 100644
--- a/public/i18n/zh.json
+++ b/public/i18n/zh.json
@@ -326,5 +326,14 @@
"notfound.description": "抱歉,您访问的页面不存在或已被移除。",
"notfound.redirect": "${countdown} 秒后自动返回首页...",
"notfound.backHome": "返回首页",
- "notfound.goBack": "返回上一页"
+ "notfound.goBack": "返回上一页",
+ "DocsTitle": "文档",
+ "DocsSearchPlaceholder": "搜索文档...",
+ "DocsOnThisPage": "本页目录",
+ "DocsNoResults": "没有找到相关文档",
+ "DocsLoading": "正在构建搜索索引...",
+ "DocsNotFound": "未找到该文档",
+ "DocsNotFoundDesc": "请求的文档不存在或暂未翻译。",
+ "DocsPrev": "上一页",
+ "DocsNext": "下一页"
}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 5201927..114a0f5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -20,6 +20,7 @@ import './App.css';
import PersonalScoresPage from './pages/PersonalScoresPage';
import QRAuthPage from './pages/QRAuthPage';
import NotFoundPage from './pages/NotFoundPage';
+import DocsPage from './pages/DocsPage';
function App() {
return (
@@ -46,6 +47,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/src/components/Docs/CodeBlock.tsx b/src/components/Docs/CodeBlock.tsx
new file mode 100644
index 0000000..250c681
--- /dev/null
+++ b/src/components/Docs/CodeBlock.tsx
@@ -0,0 +1,27 @@
+import hljs from 'highlight.js';
+import 'highlight.js/styles/github-dark.css';
+
+interface CodeBlockProps {
+ className?: string;
+ children?: React.ReactNode;
+ inline?: boolean;
+}
+
+export default function CodeBlock({ className, children, inline }: CodeBlockProps) {
+ const code = String(children ?? '').replace(/\n$/, '');
+
+ if (inline) {
+ return {children};
+ }
+
+ const language = className?.match(/language-([\w-]+)/)?.[1];
+ const highlighted = language && hljs.getLanguage(language)
+ ? hljs.highlight(code, { language }).value
+ : hljs.highlightAuto(code).value;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Docs/DocLayout.tsx b/src/components/Docs/DocLayout.tsx
new file mode 100644
index 0000000..38e09f7
--- /dev/null
+++ b/src/components/Docs/DocLayout.tsx
@@ -0,0 +1,19 @@
+interface DocLayoutProps {
+ sidebar: React.ReactNode;
+ topbar?: React.ReactNode;
+ content: React.ReactNode;
+ toc: React.ReactNode;
+}
+
+export default function DocLayout({ sidebar, topbar, content, toc }: DocLayoutProps) {
+ return (
+
+ {topbar}
+
+
{sidebar}
+
{content}
+
{toc}
+
+
+ );
+}
diff --git a/src/components/Docs/DocPagination.tsx b/src/components/Docs/DocPagination.tsx
new file mode 100644
index 0000000..f1fa380
--- /dev/null
+++ b/src/components/Docs/DocPagination.tsx
@@ -0,0 +1,48 @@
+import { Link } from 'react-router-dom';
+import { useLoc } from '@/hooks';
+import type { FlatDocItem } from '@/types';
+
+interface DocPaginationProps {
+ prev: FlatDocItem | null;
+ next: FlatDocItem | null;
+}
+
+function toDocPath(slug: string): string {
+ return slug === 'index' ? '/docs' : `/docs/${slug}`;
+}
+
+export default function DocPagination({ prev, next }: DocPaginationProps) {
+ const loc = useLoc();
+
+ if (!prev && !next) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/Docs/DocRenderer.tsx b/src/components/Docs/DocRenderer.tsx
new file mode 100644
index 0000000..33a0944
--- /dev/null
+++ b/src/components/Docs/DocRenderer.tsx
@@ -0,0 +1,98 @@
+import { useEffect, useMemo } from 'react';
+import Markdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import remarkBreaks from 'remark-breaks';
+import GithubSlugger from 'github-slugger';
+import 'github-markdown-css/github-markdown-dark.css';
+import CodeBlock from './CodeBlock';
+import type { TocHeading } from '@/types';
+
+interface DocRendererProps {
+ content: string;
+ onHeadingsChange: (headings: TocHeading[]) => void;
+}
+
+function extractText(input: React.ReactNode): string {
+ if (typeof input === 'string' || typeof input === 'number') {
+ return String(input);
+ }
+
+ if (Array.isArray(input)) {
+ return input.map(extractText).join('');
+ }
+
+ if (input && typeof input === 'object' && 'props' in input) {
+ const props = (input as { props?: { children?: React.ReactNode } }).props;
+ return extractText(props?.children);
+ }
+
+ return '';
+}
+
+function cleanHeadingText(raw: string): string {
+ return raw
+ .replace(/\[(.*?)\]\((.*?)\)/g, '$1')
+ .replace(/`([^`]+)`/g, '$1')
+ .replace(/[*_~]/g, '')
+ .trim();
+}
+
+function extractHeadings(markdown: string): TocHeading[] {
+ const slugger = new GithubSlugger();
+ const headings: TocHeading[] = [];
+ const regex = /^(#{1,4})\s+(.+)$/gm;
+ let match: RegExpExecArray | null;
+
+ while ((match = regex.exec(markdown)) !== null) {
+ const level = match[1].length;
+ const text = cleanHeadingText(match[2]);
+ const id = slugger.slug(text);
+
+ if (level >= 2 && level <= 4) {
+ headings.push({ id, text, level });
+ }
+ }
+
+ return headings;
+}
+
+export default function DocRenderer({ content, onHeadingsChange }: DocRendererProps) {
+ const headings = useMemo(() => extractHeadings(content), [content]);
+
+ useEffect(() => {
+ onHeadingsChange(headings);
+ }, [headings, onHeadingsChange]);
+
+ const slugger = new GithubSlugger();
+
+ return (
+
+ ;
+ },
+ h2(props) {
+ const id = slugger.slug(cleanHeadingText(extractText(props.children)));
+ return ;
+ },
+ h3(props) {
+ const id = slugger.slug(cleanHeadingText(extractText(props.children)));
+ return ;
+ },
+ h4(props) {
+ const id = slugger.slug(cleanHeadingText(extractText(props.children)));
+ return ;
+ },
+ code(props) {
+ return ;
+ },
+ }}
+ >
+ {content}
+
+
+ );
+}
diff --git a/src/components/Docs/DocSearchBar.tsx b/src/components/Docs/DocSearchBar.tsx
new file mode 100644
index 0000000..befd90c
--- /dev/null
+++ b/src/components/Docs/DocSearchBar.tsx
@@ -0,0 +1,47 @@
+import type { DocSearchItem } from '@/types';
+import { useLoc } from '@/hooks';
+
+interface DocSearchBarProps {
+ query: string;
+ onQueryChange: (value: string) => void;
+ results: DocSearchItem[];
+ isIndexing: boolean;
+ onSelect: (item: DocSearchItem) => void;
+}
+
+export default function DocSearchBar({ query, onQueryChange, results, isIndexing, onSelect }: DocSearchBarProps) {
+ const loc = useLoc();
+ const showResults = query.trim().length > 0;
+
+ return (
+
+
onQueryChange(event.target.value)}
+ placeholder={loc('DocsSearchPlaceholder', 'Search docs...')}
+ className="bg-black/35 px-4 py-2.5 border border-white/14 focus:border-blue-400/60 rounded-xl outline-none w-full text-white placeholder:text-white/45 text-sm transition-colors"
+ />
+
+ {showResults && (
+
+ {isIndexing &&
{loc('DocsLoading', 'Loading...')}
}
+
+ {!isIndexing && results.length === 0 && (
+
{loc('DocsNoResults', 'No matching documents')}
+ )}
+
+ {!isIndexing && results.map((item) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/Docs/DocSidebar.tsx b/src/components/Docs/DocSidebar.tsx
new file mode 100644
index 0000000..e4a75a1
--- /dev/null
+++ b/src/components/Docs/DocSidebar.tsx
@@ -0,0 +1,90 @@
+import { useMemo, useState } from 'react';
+import type { DocNavNode } from '@/types';
+
+interface DocSidebarProps {
+ items: DocNavNode[];
+ currentSlug: string;
+ onNavigate: (slug: string) => void;
+}
+
+interface SidebarNodeProps {
+ node: DocNavNode;
+ depth: number;
+ currentSlug: string;
+ onNavigate: (slug: string) => void;
+}
+
+function SidebarNode({ node, depth, currentSlug, onNavigate }: SidebarNodeProps) {
+ const defaultOpen = useMemo(() => {
+ if (!node.children?.length) return false;
+ return node.children.some((child) => child.slug === currentSlug);
+ }, [node.children, currentSlug]);
+
+ const [open, setOpen] = useState(defaultOpen);
+ const hasChildren = Boolean(node.children?.length);
+ const isActive = Boolean(node.slug && node.slug === currentSlug);
+
+ return (
+
+
+ {hasChildren ? (
+
+ ) : (
+
+ )}
+
+ {node.slug ? (
+
+ ) : (
+ {node.title}
+ )}
+
+
+ {hasChildren && open && (
+
+ {node.children!.map((child) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default function DocSidebar({ items, currentSlug, onNavigate }: DocSidebarProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Docs/DocTableOfContents.tsx b/src/components/Docs/DocTableOfContents.tsx
new file mode 100644
index 0000000..0061e22
--- /dev/null
+++ b/src/components/Docs/DocTableOfContents.tsx
@@ -0,0 +1,72 @@
+import { useEffect, useState } from 'react';
+import { useLoc } from '@/hooks';
+import type { TocHeading } from '@/types';
+
+interface DocTableOfContentsProps {
+ headings: TocHeading[];
+}
+
+export default function DocTableOfContents({ headings }: DocTableOfContentsProps) {
+ const loc = useLoc();
+ const [activeId, setActiveId] = useState('');
+
+ useEffect(() => {
+ if (headings.length === 0) {
+ setActiveId('');
+ return;
+ }
+
+ const elements = headings
+ .map((heading) => document.getElementById(heading.id))
+ .filter((element): element is HTMLElement => Boolean(element));
+
+ if (elements.length === 0) {
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const visible = entries
+ .filter((entry) => entry.isIntersecting)
+ .sort((a, b) => b.intersectionRatio - a.intersectionRatio);
+
+ if (visible.length > 0) {
+ setActiveId(visible[0].target.id);
+ }
+ },
+ {
+ rootMargin: '-25% 0px -65% 0px',
+ threshold: [0.1, 0.25, 0.5],
+ }
+ );
+
+ elements.forEach((element) => observer.observe(element));
+
+ return () => observer.disconnect();
+ }, [headings]);
+
+ if (headings.length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/Docs/index.ts b/src/components/Docs/index.ts
new file mode 100644
index 0000000..77136b1
--- /dev/null
+++ b/src/components/Docs/index.ts
@@ -0,0 +1,6 @@
+export { default as DocLayout } from './DocLayout';
+export { default as DocSidebar } from './DocSidebar';
+export { default as DocRenderer } from './DocRenderer';
+export { default as DocTableOfContents } from './DocTableOfContents';
+export { default as DocPagination } from './DocPagination';
+export { default as DocSearchBar } from './DocSearchBar';
diff --git a/src/components/UnifiedHeader/DesktopNav.tsx b/src/components/UnifiedHeader/DesktopNav.tsx
index 12455ae..1e2ad81 100644
--- a/src/components/UnifiedHeader/DesktopNav.tsx
+++ b/src/components/UnifiedHeader/DesktopNav.tsx
@@ -24,6 +24,9 @@ export default function DesktopNav() {
{loc('OriginalSongs')}
+
+ {loc('DocsTitle', 'Docs')}
+
);
}
diff --git a/src/components/UnifiedHeader/MobileNav.tsx b/src/components/UnifiedHeader/MobileNav.tsx
index c8e68d9..46647ba 100644
--- a/src/components/UnifiedHeader/MobileNav.tsx
+++ b/src/components/UnifiedHeader/MobileNav.tsx
@@ -30,11 +30,10 @@ export default function MobileNav() {
{/* 榜单项 - 可展开 */}