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() { {/* 榜单项 - 可展开 */}