修改网站markdown解析格式
This commit is contained in:
129
README.md
129
README.md
@@ -1,16 +1,127 @@
|
|||||||
# React + Vite
|
# Markdown To Web
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
此仓库将一组本地 Markdown 笔记(位于 `public/mengyanote`)转换成一个静态的 React 网站,便于浏览、检索和发布到网络上。
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
本 README 为中文说明,包含项目简介、运行/构建步骤、目录结构、如何编辑笔记以及部署和贡献指南,方便直接发布到 GitHub。
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
## 主要特性
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## React Compiler
|
- 从本地目录(`public/mengyanote`)递归读取 Markdown 文件并生成静态数据(`src/data`)。
|
||||||
|
- 使用 `react-markdown` 渲染 Markdown,支持数学公式(remark/rehype 插件)与代码高亮。
|
||||||
|
- 目录树侧边栏、内容渲染器等基础浏览功能。
|
||||||
|
- 通过简单脚本将 Markdown 文件转为项目需要的 JSON 数据,便于在静态站点中直接使用。
|
||||||
|
|
||||||
The React Compiler is not enabled on this template. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
## 技术栈
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
- React 19
|
||||||
|
- Vite
|
||||||
|
- react-markdown + remark/rehype 插件(remark-gfm、remark-math、rehype-katex、rehype-highlight 等)
|
||||||
|
|
||||||
|
## 快速开始(Windows / cmd.exe)
|
||||||
|
|
||||||
|
1. 安装依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 本地开发(会先生成静态数据)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
这会先执行 `node scripts/generateData.js` 从 `public/mengyanote` 读取 Markdown 并生成 `src/data` 下的 `directoryTree.json` / `fileContents.json` / `stats.json`,然后启动 Vite 开发服务器。
|
||||||
|
|
||||||
|
3. 生成生产构建
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 预览构建产物
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 单独生成数据(不启动服务器)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run generate-data
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 代码检查
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构(重要文件/目录)
|
||||||
|
|
||||||
|
- `public/mengyanote/` - 源 Markdown 笔记目录(请把你的 .md 文件放在这里以纳入站点)。
|
||||||
|
- `scripts/generateData.js` - 将 Markdown 文件读取并生成 `src/data` 的脚本。
|
||||||
|
- `src/data/` - 由脚本生成的 JSON 数据(目录树、文件内容、统计信息)。
|
||||||
|
- `src/components/MarkdownRenderer.jsx` - Markdown 渲染组件(样式与功能定制点)。
|
||||||
|
- `src/components/MarkdownRenderer.css` - Markdown 渲染相关样式(可在此处简化或替换为你喜欢的样式)。
|
||||||
|
- `src/components/Sidebar.jsx` - 侧边栏目录树组件。
|
||||||
|
- `src/context/AppContext.jsx` - 全局上下文与路由状态。
|
||||||
|
- `scripts/` - 工具脚本(目前包含 `generateData.js`)。
|
||||||
|
|
||||||
|
示例:如果你想让 Markdown 渲染更简洁(“换成我图片那种”样式),可以编辑 `src/components/MarkdownRenderer.css` 或直接修改 `MarkdownRenderer.jsx` 中的渲染类名/元素结构。
|
||||||
|
|
||||||
|
## 如何编辑/添加笔记
|
||||||
|
|
||||||
|
1. 在 `public/mengyanote` 下增加或修改 `.md` 文件,保持目录组织即可。
|
||||||
|
2. 运行 `npm run generate-data`(或 `npm run dev`)以重新生成 `src/data`。
|
||||||
|
3. 刷新浏览器查看最新内容。
|
||||||
|
|
||||||
|
注意:脚本会忽略以 `.` 开头的文件/目录(例如 `.obsidian`)和一些预设项(`node_modules`、`.git` 等)。
|
||||||
|
|
||||||
|
## 部署建议
|
||||||
|
|
||||||
|
- 静态站点:`npm run build` 会在 `dist/` 生成静态文件,适合部署到 GitHub Pages、Netlify、Vercel、或任意静态文件托管服务。
|
||||||
|
- GitHub Pages:构建后将 `dist/` 的内容发布到 gh-pages 分支或通过 GitHub Actions 自动发布。
|
||||||
|
|
||||||
|
示例(手工):
|
||||||
|
|
||||||
|
1. 构建
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 将 `dist/` 内容上传到你的静态托管服务或 gh-pages 分支。
|
||||||
|
|
||||||
|
如果需要,我可以为你添加一个自动部署到 GitHub Pages 的 GitHub Actions 工作流。
|
||||||
|
|
||||||
|
## 定制渲染样式
|
||||||
|
|
||||||
|
如果你觉得当前 Markdown 美化太重并想改回更简洁的“图片优先”或原始样式:
|
||||||
|
|
||||||
|
- 编辑 `src/components/MarkdownRenderer.css`:移除或覆盖过度的样式(字体、背景、代码块高亮等)。
|
||||||
|
- 编辑 `src/components/MarkdownRenderer.jsx`:调整渲染器的 className、元素包装或忽略某些 remark/rehype 插件。
|
||||||
|
|
||||||
|
常见修改点:
|
||||||
|
- 移除 KaTeX 或代码高亮:在 `src/components/MarkdownRenderer.jsx` 中删除相应的 rehype 插件 import 与使用。
|
||||||
|
- 简化图片样式:在 CSS 中将 img 的 max-width、margin 等属性调整为你想要的样式。
|
||||||
|
|
||||||
|
## 贡献与许可
|
||||||
|
|
||||||
|
欢迎提交 Issue 或 Pull Request。仓库包含 `LICENSE`(请查看该文件以确认许可证类型)。
|
||||||
|
|
||||||
|
为了保持项目干净:
|
||||||
|
- 新功能请先创建 issue 讨论。
|
||||||
|
- 提交 PR 时附带简短说明和相关截图(如果是 UI 变更)。
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如需帮助或定制:在 Issue 中描述你想要的样式和示例图片/链接,我可以帮你修改 `MarkdownRenderer.css` / `MarkdownRenderer.jsx` 实现视觉风格。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
祝发布顺利!如果你想,我可以:
|
||||||
|
|
||||||
|
- 帮你把 README 翻译成英文版本并放在 `README.en.md`;
|
||||||
|
- 或直接替你修改 Markdown 渲染样式(把渲染改成更像你“图片那种”的风格)。
|
||||||
|
|
||||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
|
||||||
|
|||||||
8
public/mengyanote/.obsidian/workspace.json
vendored
8
public/mengyanote/.obsidian/workspace.json
vendored
@@ -13,12 +13,12 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "树萌芽的小本本/目前已部署网站.md",
|
"file": "编程语言/Android/Linux配置安卓Gradle构建环境.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false
|
"source": false
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "目前已部署网站"
|
"title": "Linux配置安卓Gradle构建环境"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -186,8 +186,9 @@
|
|||||||
},
|
},
|
||||||
"active": "8304b0e105b08ed0",
|
"active": "8304b0e105b08ed0",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"树萌芽的小本本/网站小技巧.md",
|
"编程语言/Android/安卓Gradle构建常用命令总结.md",
|
||||||
"树萌芽的小本本/目前已部署网站.md",
|
"树萌芽的小本本/目前已部署网站.md",
|
||||||
|
"树萌芽的小本本/网站小技巧.md",
|
||||||
"Docker/优秀好用的Docker镜像/FileCodeBox-文件快递柜.md",
|
"Docker/优秀好用的Docker镜像/FileCodeBox-文件快递柜.md",
|
||||||
"Docker/优秀好用的Docker镜像/Postgres数据库.md",
|
"Docker/优秀好用的Docker镜像/Postgres数据库.md",
|
||||||
"Docker/优秀好用的Docker镜像/未命名.md",
|
"Docker/优秀好用的Docker镜像/未命名.md",
|
||||||
@@ -207,7 +208,6 @@
|
|||||||
"Docker/Docker镜像快速迁移.md",
|
"Docker/Docker镜像快速迁移.md",
|
||||||
"无线-HCIA 02.md",
|
"无线-HCIA 02.md",
|
||||||
"Linux相关/把Ubuntu镜像源切换到阿里云.md",
|
"Linux相关/把Ubuntu镜像源切换到阿里云.md",
|
||||||
"编程语言/Android/安卓Gradle构建常用命令总结.md",
|
|
||||||
"临时解决方案/修改hosts方式来直连Github.md",
|
"临时解决方案/修改hosts方式来直连Github.md",
|
||||||
"临时解决方案/萌芽云剪切板.md",
|
"临时解决方案/萌芽云剪切板.md",
|
||||||
"2025年9月紧急规划.md",
|
"2025年9月紧急规划.md",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Android Gradle 常用命令速查(基于 `./gradlew`)
|
|
||||||
|
|
||||||
|
|
||||||
**基础**
|
##### **基础**
|
||||||
|
|
||||||
- `./gradlew tasks`
|
- `./gradlew tasks`
|
||||||
列出可用的 Gradle 任务(查看当前项目能跑什么任务)。
|
列出可用的 Gradle 任务(查看当前项目能跑什么任务)。
|
||||||
@@ -13,7 +12,7 @@
|
|||||||
清理构建产物(删除 `build/` 目录)。
|
清理构建产物(删除 `build/` 目录)。
|
||||||
|
|
||||||
|
|
||||||
**构建 APK / AAB**
|
##### **构建 APK / AAB**
|
||||||
|
|
||||||
- `./gradlew assembleDebug`
|
- `./gradlew assembleDebug`
|
||||||
构建 debug APK(输出:`app/build/outputs/apk/debug/*.apk`)。
|
构建 debug APK(输出:`app/build/outputs/apk/debug/*.apk`)。
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
生成 debug bundle(少用,通常用于测试)。
|
生成 debug bundle(少用,通常用于测试)。
|
||||||
|
|
||||||
|
|
||||||
**按 module / productFlavor / buildType 构建**
|
##### **按 module / productFlavor / buildType 构建**
|
||||||
|
|
||||||
- `./gradlew :moduleName:assembleRelease`
|
- `./gradlew :moduleName:assembleRelease`
|
||||||
构建指定 module(多模块项目时用)。
|
构建指定 module(多模块项目时用)。
|
||||||
@@ -37,7 +36,7 @@
|
|||||||
构建指定 flavor + buildType(例如 `assemblePaidRelease`)。
|
构建指定 flavor + buildType(例如 `assemblePaidRelease`)。
|
||||||
|
|
||||||
|
|
||||||
**安装与卸载**
|
##### **安装与卸载**
|
||||||
|
|
||||||
- `./gradlew installDebug`
|
- `./gradlew installDebug`
|
||||||
将 debug APK 安装到连接的设备/模拟器(需要 adb 可用)。
|
将 debug APK 安装到连接的设备/模拟器(需要 adb 可用)。
|
||||||
@@ -48,14 +47,14 @@
|
|||||||
- 如果用生成的 APK 手动安装:`adb install -r app/build/outputs/apk/debug/app-debug.apk`
|
- 如果用生成的 APK 手动安装:`adb install -r app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
|
||||||
|
|
||||||
**测试**
|
##### **测试**
|
||||||
|
|
||||||
- 单元测试(JVM):`./gradlew test` 或 `./gradlew testDebugUnitTest`
|
- 单元测试(JVM):`./gradlew test` 或 `./gradlew testDebugUnitTest`
|
||||||
|
|
||||||
- 仪器/设备测试(connected devices):`./gradlew connectedAndroidTest` 或 `./gradlew connectedCheck`
|
- 仪器/设备测试(connected devices):`./gradlew connectedAndroidTest` 或 `./gradlew connectedCheck`
|
||||||
|
|
||||||
|
|
||||||
**静态检查 / 报表**
|
##### **静态检查 / 报表**
|
||||||
|
|
||||||
- `./gradlew lint` 或 `./gradlew lintDebug`
|
- `./gradlew lint` 或 `./gradlew lintDebug`
|
||||||
运行 Android Lint。
|
运行 Android Lint。
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
输出签名信息(SHA1/SHA256),常用于配置 API key(Google/Firebase)。
|
输出签名信息(SHA1/SHA256),常用于配置 API key(Google/Firebase)。
|
||||||
|
|
||||||
|
|
||||||
**调试构建问题的常用参数**
|
##### **调试构建问题的常用参数**
|
||||||
|
|
||||||
- `--stacktrace` / `--full-stacktrace`:打印堆栈跟踪(排错用)
|
- `--stacktrace` / `--full-stacktrace`:打印堆栈跟踪(排错用)
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@
|
|||||||
- `--refresh-dependencies`:刷新依赖缓存
|
- `--refresh-dependencies`:刷新依赖缓存
|
||||||
|
|
||||||
|
|
||||||
**性能 / CI 常用组合示例**
|
##### **性能 / CI 常用组合示例**
|
||||||
|
|
||||||
- 本地快速一把:`./gradlew clean assembleDebug --parallel --info`
|
- 本地快速一把:`./gradlew clean assembleDebug --parallel --info`
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
- 只构建 moduleA 的 release:`./gradlew :moduleA:assembleRelease`
|
- 只构建 moduleA 的 release:`./gradlew :moduleA:assembleRelease`
|
||||||
|
|
||||||
|
|
||||||
**常见路径**
|
##### **常见路径**
|
||||||
|
|
||||||
- APK:`app/build/outputs/apk/<buildType|flavor>/...`
|
- APK:`app/build/outputs/apk/<buildType|flavor>/...`
|
||||||
|
|
||||||
@@ -101,13 +100,10 @@
|
|||||||
- 临时构建缓存:`~/.gradle/caches/`
|
- 临时构建缓存:`~/.gradle/caches/`
|
||||||
|
|
||||||
|
|
||||||
**小贴士**
|
##### **小贴士**
|
||||||
|
|
||||||
- 始终用项目里的 Gradle Wrapper(`./gradlew`),保证 Gradle 版本一致。
|
- 始终用项目里的 Gradle Wrapper(`./gradlew`),保证 Gradle 版本一致。
|
||||||
|
|
||||||
- Release 构建需要正确的 `signingConfig`(通常放在 `gradle.properties` + `build.gradle`),也可以在 CI 用 `-P` 传参数(注意不要把敏感信息放在日志里)。
|
- Release 构建需要正确的 `signingConfig`(通常放在 `gradle.properties` + `build.gradle`),也可以在 CI 用 `-P` 传参数(注意不要把敏感信息放在日志里)。
|
||||||
|
|
||||||
- 出问题先加 `--stacktrace --info` 看详情,再定位是依赖、ProGuard/R8、签名还是资源冲突。
|
- 出问题先加 `--stacktrace --info` 看详情,再定位是依赖、ProGuard/R8、签名还是资源冲突。
|
||||||
|
|
||||||
|
|
||||||
如果你想,我可以把你常用的几条命令做成一个可复用的脚本/CI job 模板(GitHub Actions/GitLab CI),或者按你项目的 flavor 给出精确的 assemble 命令。要哪个直接说。
|
|
||||||
59
src/App.css
59
src/App.css
@@ -1,19 +1,17 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-bg: #eafbf2;
|
--color-bg: #f5f7fb;
|
||||||
--color-bg-secondary: #d6f3e3;
|
--color-surface: #ffffff;
|
||||||
--color-surface: rgba(255, 255, 255, 0.88);
|
--color-surface-alt: #f1f4fa;
|
||||||
--color-surface-strong: #ffffff;
|
--color-border: #d8deed;
|
||||||
--color-border: rgba(47, 177, 112, 0.2);
|
--color-text: #1f2a44;
|
||||||
--color-text: #1f3a2c;
|
--color-muted: #6f7b92;
|
||||||
--color-muted: #4a6c5a;
|
--color-accent: #3a7afe;
|
||||||
--color-link: #269c66;
|
--color-accent-soft: rgba(58, 122, 254, 0.1);
|
||||||
--color-link-hover: #1e7a51;
|
--color-danger: #d6455d;
|
||||||
--color-accent: #33c57a;
|
--sidebar-width: clamp(250px, 20vw, 320px);
|
||||||
--color-accent-contrast: #0f3322;
|
--shadow-soft: 0 20px 45px rgba(86, 105, 141, 0.15);
|
||||||
--shadow-soft: 0 18px 38px rgba(36, 118, 74, 0.18);
|
--radius-lg: 22px;
|
||||||
--radius-lg: 24px;
|
--font-family-base: 'Inter', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
--sidebar-width: clamp(240px, 21vw, 320px);
|
|
||||||
--gradient-bg: linear-gradient(135deg, #f2fff8 0%, #c9f4df 40%, #a6e7d0 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -25,32 +23,32 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--gradient-bg);
|
background: linear-gradient(135deg, #f6f9ff 0%, #edf2fb 45%, #fbfdff 100%);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: 'Inter', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
font-family: var(--font-family-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: clamp(0.75rem, 2vw, 2rem);
|
padding: clamp(1rem, 4vw, 2.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: calc(100vh - clamp(1.5rem, 4vw, 4rem));
|
min-height: calc(100vh - clamp(2rem, 6vw, 5rem));
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
backdrop-filter: blur(16px);
|
border: 1px solid var(--color-border);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::selection {
|
body ::selection {
|
||||||
background-color: rgba(51, 197, 122, 0.35);
|
background-color: rgba(58, 122, 254, 0.18);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -58,38 +56,39 @@ body ::selection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: rgba(216, 222, 237, 0.4);
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(43, 156, 105, 0.25);
|
background: rgba(111, 123, 146, 0.35);
|
||||||
border-radius: 9999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(43, 156, 105, 0.4);
|
background: rgba(111, 123, 146, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
a:focus {
|
a:focus {
|
||||||
outline: 3px solid rgba(51, 197, 122, 0.45);
|
outline: 2px solid rgba(58, 122, 254, 0.45);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
#root {
|
#root {
|
||||||
padding: clamp(0.5rem, 3vw, 1.25rem);
|
padding: clamp(0.75rem, 4vw, 1.75rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
min-height: calc(100vh - clamp(1rem, 6vw, 3rem));
|
min-height: calc(100vh - clamp(1.5rem, 7vw, 3.5rem));
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
#root {
|
#root {
|
||||||
padding: clamp(0.5rem, 4vw, 1rem);
|
padding: clamp(0.5rem, 5vw, 1.2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
|||||||
@@ -2,172 +2,68 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(233, 250, 240, 0.92) 100%);
|
background: var(--color-surface);
|
||||||
overflow-y: auto;
|
color: var(--color-text);
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-area.with-sidebar {
|
.content-area.with-sidebar {
|
||||||
border-left: 1px solid rgba(47, 177, 112, 0.12);
|
border-left: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-header {
|
.content-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 10;
|
||||||
padding: clamp(1.25rem, 3vw, 2rem) clamp(1.25rem, 4vw, 2.5rem) clamp(0.75rem, 2vw, 1.25rem);
|
padding: 1.75rem clamp(2rem, 5vw, 3rem) 1.25rem;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: var(--color-surface);
|
||||||
backdrop-filter: blur(12px);
|
border-bottom: 1px solid var(--color-border);
|
||||||
border-bottom: 1px solid rgba(47, 177, 112, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.35rem;
|
gap: 0.45rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-item {
|
.breadcrumb-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-separator {
|
.breadcrumb-separator {
|
||||||
color: rgba(47, 177, 112, 0.35);
|
color: rgba(31, 42, 68, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-text {
|
.breadcrumb-text {
|
||||||
padding: 0.25rem 0.55rem;
|
padding: 0.2rem 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(51, 197, 122, 0.12);
|
background: rgba(58, 122, 254, 0.08);
|
||||||
transition: background 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-text:hover {
|
|
||||||
background: rgba(51, 197, 122, 0.22);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-title {
|
.content-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(1.8rem, 3vw, 2.4rem);
|
font-size: clamp(2rem, 3.5vw, 2.6rem);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-body {
|
.content-body {
|
||||||
padding: clamp(1.5rem, 4vw, 3rem) clamp(1.25rem, 6vw, 3.5rem) clamp(2.5rem, 6vw, 4rem);
|
flex: 1;
|
||||||
width: 100%;
|
overflow-y: auto;
|
||||||
max-width: 1200px;
|
display: flex;
|
||||||
margin: 0 auto;
|
justify-content: center;
|
||||||
display: grid;
|
padding: clamp(2rem, 6vw, 3.5rem);
|
||||||
gap: clamp(2rem, 4vw, 3.5rem);
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-body.with-toc {
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 260px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-pane {
|
.markdown-pane {
|
||||||
min-width: 0;
|
width: min(860px, 100%);
|
||||||
}
|
|
||||||
|
|
||||||
.content-toc {
|
|
||||||
position: sticky;
|
|
||||||
top: clamp(6rem, 12vw, 7rem);
|
|
||||||
align-self: start;
|
|
||||||
padding: 1.25rem 1.1rem;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: rgba(255, 255, 255, 0.84);
|
|
||||||
border: 1px solid rgba(47, 177, 112, 0.14);
|
|
||||||
box-shadow: 0 16px 32px rgba(31, 58, 44, 0.12);
|
|
||||||
max-height: calc(100vh - clamp(8rem, 14vw, 10rem));
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-toc h3 {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-muted);
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-item {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-link {
|
|
||||||
display: block;
|
|
||||||
padding: 0.35rem 0.6rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-link.level-2 {
|
|
||||||
padding-left: 1.15rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-link.level-3 {
|
|
||||||
padding-left: 1.85rem;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-link:hover,
|
|
||||||
.toc-link.active {
|
|
||||||
background: rgba(51, 197, 122, 0.18);
|
|
||||||
color: var(--color-text);
|
|
||||||
transform: translateX(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.65rem;
|
|
||||||
padding: 4rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border: 2px solid rgba(51, 197, 122, 0.2);
|
|
||||||
border-top: 2px solid var(--color-accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content {
|
.markdown-content {
|
||||||
color: var(--color-text);
|
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
@@ -182,31 +78,31 @@
|
|||||||
.markdown-content h4,
|
.markdown-content h4,
|
||||||
.markdown-content h5,
|
.markdown-content h5,
|
||||||
.markdown-content h6 {
|
.markdown-content h6 {
|
||||||
margin: 2.2rem 0 1rem;
|
margin: 2rem 0 1rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.25;
|
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content h1 {
|
.markdown-content h1 {
|
||||||
font-size: clamp(2rem, 3vw, 2.6rem);
|
font-size: clamp(2.1rem, 3vw, 2.8rem);
|
||||||
border-bottom: 2px solid rgba(47, 177, 112, 0.14);
|
border-bottom: 2px solid rgba(31, 42, 68, 0.08);
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content h2 {
|
.markdown-content h2 {
|
||||||
font-size: clamp(1.6rem, 2.4vw, 2.1rem);
|
font-size: clamp(1.7rem, 2.5vw, 2.2rem);
|
||||||
border-bottom: 1px solid rgba(47, 177, 112, 0.16);
|
border-bottom: 1px solid rgba(31, 42, 68, 0.08);
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content h3 {
|
.markdown-content h3 {
|
||||||
font-size: clamp(1.35rem, 2vw, 1.6rem);
|
font-size: clamp(1.4rem, 2vw, 1.7rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content h4 {
|
.markdown-content h4 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-anchor {
|
.heading-anchor {
|
||||||
@@ -214,16 +110,16 @@
|
|||||||
left: -1.5rem;
|
left: -1.5rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
opacity: 0;
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: rgba(47, 177, 112, 0.8);
|
color: rgba(31, 42, 68, 0.35);
|
||||||
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-content h1:hover .heading-anchor,
|
||||||
.markdown-content h2:hover .heading-anchor,
|
.markdown-content h2:hover .heading-anchor,
|
||||||
.markdown-content h3:hover .heading-anchor,
|
.markdown-content h3:hover .heading-anchor,
|
||||||
.markdown-content h4:hover .heading-anchor,
|
.markdown-content h4:hover .heading-anchor {
|
||||||
.markdown-content h1:hover .heading-anchor {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate(-4px, -50%);
|
transform: translate(-4px, -50%);
|
||||||
}
|
}
|
||||||
@@ -232,10 +128,9 @@
|
|||||||
.markdown-content ul,
|
.markdown-content ul,
|
||||||
.markdown-content ol,
|
.markdown-content ol,
|
||||||
.markdown-content blockquote,
|
.markdown-content blockquote,
|
||||||
.markdown-content table,
|
.markdown-content pre,
|
||||||
.markdown-content pre {
|
.markdown-content table {
|
||||||
margin-top: 0;
|
margin: 0 0 1.35rem;
|
||||||
margin-bottom: 1.35rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content ul,
|
.markdown-content ul,
|
||||||
@@ -248,32 +143,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content a {
|
.markdown-content a {
|
||||||
color: var(--color-link);
|
color: var(--color-accent);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
text-underline-offset: 4px;
|
text-underline-offset: 3px;
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a:hover {
|
|
||||||
color: var(--color-link-hover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-code {
|
.inline-code {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.1rem 0.35rem;
|
padding: 0.15rem 0.4rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: rgba(33, 169, 102, 0.12);
|
background: rgba(58, 122, 254, 0.08);
|
||||||
border: 1px solid rgba(33, 169, 102, 0.18);
|
border: 1px solid rgba(58, 122, 254, 0.16);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-block-wrapper {
|
.code-block-wrapper {
|
||||||
position: relative;
|
background: #f6f8ff;
|
||||||
background: #0f2c1f;
|
border: 1px solid rgba(58, 122, 254, 0.15);
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 18px 36px rgba(12, 52, 34, 0.45);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,75 +172,99 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(58, 122, 254, 0.08);
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
color: var(--color-muted);
|
||||||
color: #bcf8d9;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.08em;
|
||||||
}
|
text-transform: uppercase;
|
||||||
|
|
||||||
.code-language {
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-copy-button {
|
.code-copy-button {
|
||||||
background: rgba(188, 248, 217, 0.14);
|
border: 1px solid rgba(58, 122, 254, 0.25);
|
||||||
color: #bcf8d9;
|
background: #fff;
|
||||||
border: 1px solid rgba(188, 248, 217, 0.25);
|
color: var(--color-muted);
|
||||||
border-radius: 999px;
|
border-radius: 8px;
|
||||||
padding: 0.35rem 0.75rem;
|
padding: 0.35rem 0.7rem;
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease, transform 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-copy-button:hover {
|
.code-copy-button:hover {
|
||||||
background: rgba(188, 248, 217, 0.24);
|
color: var(--color-accent);
|
||||||
transform: translateY(-1px);
|
border-color: rgba(58, 122, 254, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-copy-button.copied {
|
.code-copy-button.copied {
|
||||||
background: rgba(188, 248, 217, 0.4);
|
color: var(--color-accent);
|
||||||
color: #0f2c1f;
|
border-color: rgba(58, 122, 254, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1.1rem 1.35rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: #e9fff2;
|
color: #1f2a44;
|
||||||
font-size: 0.95rem;
|
}
|
||||||
|
|
||||||
|
.markdown-image {
|
||||||
|
margin: 1.6rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-image img {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(31, 42, 68, 0.1);
|
||||||
|
box-shadow: 0 18px 38px rgba(31, 42, 68, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-image figcaption {
|
||||||
|
margin-top: 0.55rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
margin: 1.6rem 0;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-left: 4px solid rgba(58, 122, 254, 0.45);
|
||||||
|
background: rgba(58, 122, 254, 0.1);
|
||||||
|
border-radius: 0 14px 14px 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px dashed rgba(31, 42, 68, 0.12);
|
||||||
|
margin: 2.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper {
|
.table-wrapper {
|
||||||
width: 100%;
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
border: 1px solid rgba(31, 42, 68, 0.12);
|
||||||
border: 1px solid rgba(47, 177, 112, 0.14);
|
background: #ffffff;
|
||||||
box-shadow: 0 14px 28px rgba(31, 58, 44, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper table {
|
.table-wrapper table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper th,
|
.table-wrapper th,
|
||||||
.table-wrapper td {
|
.table-wrapper td {
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
border-bottom: 1px solid rgba(47, 177, 112, 0.12);
|
border-bottom: 1px solid rgba(31, 42, 68, 0.1);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper th {
|
.table-wrapper th {
|
||||||
background: rgba(51, 197, 122, 0.12);
|
color: var(--color-muted);
|
||||||
color: var(--color-text);
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,205 +272,117 @@ pre code {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper tr:hover td {
|
|
||||||
background: rgba(51, 197, 122, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-blockquote {
|
|
||||||
margin: 1.75rem 0;
|
|
||||||
padding: 1.1rem 1.4rem 1.1rem 1.2rem;
|
|
||||||
border-left: 5px solid rgba(47, 177, 112, 0.6);
|
|
||||||
border-radius: 0 16px 16px 0;
|
|
||||||
background: rgba(51, 197, 122, 0.12);
|
|
||||||
color: var(--color-text);
|
|
||||||
box-shadow: 0 12px 24px rgba(31, 58, 44, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-blockquote strong:first-child {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px dashed rgba(47, 177, 112, 0.35);
|
|
||||||
margin: 2.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content img {
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 18px 36px rgba(31, 58, 44, 0.18);
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-image {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-image figcaption {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-blockquote.callout-info {
|
|
||||||
border-left-color: rgba(70, 199, 150, 0.7);
|
|
||||||
background: rgba(70, 199, 150, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-blockquote.callout-warning {
|
|
||||||
border-left-color: rgba(255, 187, 92, 0.85);
|
|
||||||
background: rgba(255, 187, 92, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-blockquote.callout-danger {
|
|
||||||
border-left-color: rgba(255, 107, 107, 0.85);
|
|
||||||
background: rgba(255, 107, 107, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-blockquote.callout-success {
|
|
||||||
border-left-color: rgba(62, 201, 133, 0.85);
|
|
||||||
background: rgba(62, 201, 133, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.external-link {
|
.external-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.external-link-icon {
|
.external-link-icon {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
padding: 3rem 0;
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid rgba(111, 123, 146, 0.25);
|
||||||
|
border-top: 2px solid var(--color-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.welcome-message {
|
.welcome-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: clamp(2rem, 6vw, 4rem);
|
padding: clamp(2rem, 6vw, 4rem);
|
||||||
min-height: calc(100vh - clamp(10rem, 16vw, 12rem));
|
min-height: calc(100vh - 7rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-content {
|
.welcome-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 640px;
|
max-width: 520px;
|
||||||
padding: clamp(2rem, 6vw, 3rem);
|
padding: 2.4rem;
|
||||||
border-radius: 28px;
|
border-radius: 18px;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #ffffff;
|
||||||
box-shadow: 0 28px 48px rgba(31, 58, 44, 0.16);
|
border: 1px solid var(--color-border);
|
||||||
|
box-shadow: 0 24px 48px rgba(86, 105, 141, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-content h1 {
|
.welcome-content h1 {
|
||||||
font-size: clamp(2.4rem, 4vw, 3rem);
|
margin-bottom: 0.9rem;
|
||||||
color: var(--color-text);
|
font-size: clamp(2.1rem, 3.2vw, 2.6rem);
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-content p {
|
.welcome-content p {
|
||||||
font-size: 1.1rem;
|
margin: 0 auto 1.6rem;
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-features {
|
.welcome-features {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
gap: 1.1rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-item {
|
.feature-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: 0.6rem;
|
||||||
padding: 1rem;
|
padding: 0.85rem;
|
||||||
background: rgba(51, 197, 122, 0.12);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(47, 177, 112, 0.14);
|
background: var(--color-surface);
|
||||||
color: var(--color-text);
|
color: var(--color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-icon {
|
.feature-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
}
|
|
||||||
|
|
||||||
.katex-display {
|
|
||||||
padding: 1.25rem;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(51, 197, 122, 0.1);
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .task-list-item {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .task-list-item input[type='checkbox'] {
|
|
||||||
margin-right: 0.6rem;
|
|
||||||
transform: scale(1.1);
|
|
||||||
accent-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.content-body.with-toc {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-toc {
|
|
||||||
position: relative;
|
|
||||||
top: auto;
|
|
||||||
max-height: none;
|
|
||||||
order: -1;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.content-header {
|
.content-header {
|
||||||
padding: 1rem 1.25rem;
|
padding: 1.2rem 1.5rem 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-body {
|
.content-body {
|
||||||
padding: 1.25rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-title {
|
.markdown-pane {
|
||||||
font-size: clamp(1.6rem, 6vw, 2rem);
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content {
|
|
||||||
font-size: 0.98rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-anchor {
|
.heading-anchor {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-content {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-features {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.content-header {
|
.content-header {
|
||||||
padding: 0.85rem 1rem;
|
padding: 1rem 1.1rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-body {
|
.content-body {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-item {
|
.feature-item {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
@@ -15,6 +15,7 @@ import 'highlight.js/styles/github.css';
|
|||||||
function Breadcrumbs({ filePath }) {
|
function Breadcrumbs({ filePath }) {
|
||||||
const breadcrumbs = generateBreadcrumbs(filePath);
|
const breadcrumbs = generateBreadcrumbs(filePath);
|
||||||
if (breadcrumbs.length === 0) return null;
|
if (breadcrumbs.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="breadcrumbs" aria-label="当前位置">
|
<nav className="breadcrumbs" aria-label="当前位置">
|
||||||
{breadcrumbs.map((crumb, index) => (
|
{breadcrumbs.map((crumb, index) => (
|
||||||
@@ -28,8 +29,7 @@ function Breadcrumbs({ filePath }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CodeBlock({ inline, className, children, ...props }) {
|
function CodeBlock({ inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const [copied, setCopied] = useState(false);
|
||||||
const language = match ? match[1] : '';
|
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
return (
|
return (
|
||||||
@@ -39,13 +39,34 @@ function CodeBlock({ inline, className, children, ...props }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codeText = React.Children.toArray(children).join('').replace(/\n$/, '');
|
||||||
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
const language = match ? match[1] : 'text';
|
||||||
|
const buttonClass = 'code-copy-button' + (copied ? ' copied' : '');
|
||||||
|
const buttonLabel = copied ? '已复制' : '复制代码';
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(codeText);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy code block', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="code-block-wrapper">
|
<div className="code-block-wrapper">
|
||||||
{language && (
|
<div className="code-block-header">
|
||||||
<div className="code-block-header">
|
<span className="code-language">{language}</span>
|
||||||
<span className="code-language">{language}</span>
|
<button type="button" className={buttonClass} onClick={handleCopy} aria-live="polite">
|
||||||
</div>
|
{buttonLabel}
|
||||||
)}
|
</button>
|
||||||
|
</div>
|
||||||
<pre className={className} {...props}>
|
<pre className={className} {...props}>
|
||||||
<code>{children}</code>
|
<code>{children}</code>
|
||||||
</pre>
|
</pre>
|
||||||
@@ -57,19 +78,21 @@ function CustomLink({ href, children, ...props }) {
|
|||||||
if (href && href.startsWith('[[') && href.endsWith(']]')) {
|
if (href && href.startsWith('[[') && href.endsWith(']]')) {
|
||||||
const linkText = href.slice(2, -2);
|
const linkText = href.slice(2, -2);
|
||||||
return (
|
return (
|
||||||
<span className="internal-link" title={内部链接: }>
|
<span className="internal-link" title={`内部链接: ${linkText}`}>
|
||||||
{children || linkText}
|
{children || linkText}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExternal = href && /^(https?:)?\/\//.test(href);
|
const isExternal = href && /^(https?:)?\/\//.test(href);
|
||||||
|
const linkClass = isExternal ? 'external-link' : 'internal-link';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
target={isExternal ? '_blank' : '_self'}
|
target={isExternal ? '_blank' : '_self'}
|
||||||
rel={isExternal ? 'noopener noreferrer' : undefined}
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||||
className={isExternal ? 'external-link' : 'internal-link'}
|
className={linkClass}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -86,97 +109,8 @@ function CustomTable({ children, ...props }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHeadings(content) {
|
function createHeadingRenderer(tag) {
|
||||||
return useMemo(() => {
|
return function HeadingRenderer({ children, ...props }) {
|
||||||
const headingRegex = /^(#{1,4})\s+(.+)$/gm;
|
|
||||||
const headings = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = headingRegex.exec(content)) !== null) {
|
|
||||||
const [, hashes, text] = match;
|
|
||||||
const level = hashes.length;
|
|
||||||
if (level > 4) continue;
|
|
||||||
|
|
||||||
const plainText = text.replace(/[*_~]/g, '').trim();
|
|
||||||
const id = plainText
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
|
|
||||||
.replace(/\s+/g, '-');
|
|
||||||
|
|
||||||
headings.push({ id, text: plainText, level });
|
|
||||||
}
|
|
||||||
|
|
||||||
return headings;
|
|
||||||
}, [content]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableOfContents({ headings }) {
|
|
||||||
if (headings.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="content-toc" aria-label="目录">
|
|
||||||
<h3>目录</h3>
|
|
||||||
<ul className="toc-list">
|
|
||||||
{headings.map((heading) => (
|
|
||||||
<li key={heading.id} className="toc-item">
|
|
||||||
<a
|
|
||||||
className={ oc-link level-}
|
|
||||||
href={#}
|
|
||||||
>
|
|
||||||
{heading.text}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MarkdownRenderer() {
|
|
||||||
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
|
|
||||||
const [activeHeading, setActiveHeading] = useState(null);
|
|
||||||
const headings = useHeadings(currentContent);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === 'undefined' || headings.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const visible = entries
|
|
||||||
.filter((entry) => entry.isIntersecting)
|
|
||||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
|
||||||
|
|
||||||
if (visible[0]) {
|
|
||||||
setActiveHeading(visible[0].target.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rootMargin: '-40% 0px -50% 0px',
|
|
||||||
threshold: [0, 1],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
headings.forEach((heading) => {
|
|
||||||
const element = document.getElementById(heading.id);
|
|
||||||
if (element) {
|
|
||||||
observer.observe(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
headings.forEach((heading) => {
|
|
||||||
const element = document.getElementById(heading.id);
|
|
||||||
if (element) {
|
|
||||||
observer.unobserve(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [headings]);
|
|
||||||
|
|
||||||
const renderHeading = useCallback((Tag) => ({ children, ...props }) => {
|
|
||||||
const text = React.Children.toArray(children)
|
const text = React.Children.toArray(children)
|
||||||
.map((child) => {
|
.map((child) => {
|
||||||
if (typeof child === 'string') return child;
|
if (typeof child === 'string') return child;
|
||||||
@@ -190,111 +124,74 @@ export default function MarkdownRenderer() {
|
|||||||
|
|
||||||
const id = text
|
const id = text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
|
.replace(/[^a-z0-9\u00c0-\u024f\u4e00-\u9fa5\s-]/g, '')
|
||||||
.replace(/\s+/g, '-');
|
.replace(/\s+/g, '-');
|
||||||
|
|
||||||
|
const HeadingTag = tag;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag id={id} {...props}>
|
<HeadingTag id={id} {...props}>
|
||||||
<a href={#} aria-hidden className="heading-anchor">
|
<a href={`#${id}`} aria-hidden className="heading-anchor">
|
||||||
#
|
#
|
||||||
</a>
|
</a>
|
||||||
{children}
|
{children}
|
||||||
</Tag>
|
</HeadingTag>
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const components = useMemo(() => ({
|
const headingComponents = {
|
||||||
code: CodeBlock,
|
h1: createHeadingRenderer('h1'),
|
||||||
a: CustomLink,
|
h2: createHeadingRenderer('h2'),
|
||||||
table: CustomTable,
|
h3: createHeadingRenderer('h3'),
|
||||||
h1: renderHeading('h1'),
|
h4: createHeadingRenderer('h4'),
|
||||||
h2: renderHeading('h2'),
|
};
|
||||||
h3: renderHeading('h3'),
|
|
||||||
h4: renderHeading('h4'),
|
|
||||||
blockquote: ({ children, ...props }) => {
|
|
||||||
const childText = React.Children.toArray(children)
|
|
||||||
.map((child) => {
|
|
||||||
if (typeof child === 'string') return child.trim();
|
|
||||||
if (
|
|
||||||
React.isValidElement(child) &&
|
|
||||||
typeof child.props.children === 'string'
|
|
||||||
) {
|
|
||||||
return child.props.children.trim();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const calloutMatch = childText.match(/^\s*\[(info|warning|danger|success)\]\s*/i);
|
export default function MarkdownRenderer() {
|
||||||
const calloutType = calloutMatch ? calloutMatch[1].toLowerCase() : null;
|
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
|
||||||
|
|
||||||
return (
|
const components = useMemo(
|
||||||
<blockquote
|
() => ({
|
||||||
className={custom-blockquote}
|
code: CodeBlock,
|
||||||
{...props}
|
a: CustomLink,
|
||||||
>
|
table: CustomTable,
|
||||||
|
...headingComponents,
|
||||||
|
blockquote: ({ children, ...props }) => (
|
||||||
|
<blockquote className="custom-blockquote" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
),
|
||||||
},
|
img: ({ alt, ...props }) => (
|
||||||
img: ({ alt, ...props }) => (
|
<figure className="markdown-image">
|
||||||
<figure className="markdown-image">
|
<img alt={alt} {...props} />
|
||||||
<img alt={alt} {...props} />
|
{alt && <figcaption>{alt}</figcaption>}
|
||||||
{alt && <figcaption>{alt}</figcaption>}
|
</figure>
|
||||||
</figure>
|
),
|
||||||
),
|
}),
|
||||||
ol: ({ ordered, ...props }) => <ol className="ordered-list" {...props} />,
|
[],
|
||||||
ul: ({ ordered, ...props }) => <ul className="unordered-list" {...props} />,
|
);
|
||||||
li: ({ checked, children, ...props }) => (
|
|
||||||
<li
|
|
||||||
className={list-item}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</li>
|
|
||||||
),
|
|
||||||
}), [renderHeading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const contentAreaClass = 'content-area' + (sidebarOpen ? ' with-sidebar' : '');
|
||||||
if (!activeHeading) return undefined;
|
|
||||||
|
|
||||||
const tocLinks = document.querySelectorAll('.toc-link');
|
|
||||||
tocLinks.forEach((link) => {
|
|
||||||
if (link.getAttribute('href') === #) {
|
|
||||||
link.classList.add('active');
|
|
||||||
} else {
|
|
||||||
link.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
tocLinks.forEach((link) => link.classList.remove('active'));
|
|
||||||
};
|
|
||||||
}, [activeHeading, headings]);
|
|
||||||
|
|
||||||
if (!currentFile) {
|
if (!currentFile) {
|
||||||
return (
|
return (
|
||||||
<div className={content-area }>
|
<div className={contentAreaClass}>
|
||||||
<div className="welcome-message">
|
<div className="welcome-message">
|
||||||
<div className="welcome-content">
|
<div className="welcome-content">
|
||||||
<h1>🌱 欢迎来到萌芽笔记</h1>
|
<h1>🌙 欢迎回来</h1>
|
||||||
<p>请从左侧导航栏选择一个笔记文件开始阅读</p>
|
<p>从左侧目录选择任意 Markdown 笔记即可开始阅读。</p>
|
||||||
<div className="welcome-features">
|
<div className="welcome-features">
|
||||||
<div className="feature-item">
|
<div className="feature-item">
|
||||||
<span className="feature-icon">📝</span>
|
<span className="feature-icon">📝</span>
|
||||||
<span>支持完整的Markdown语法</span>
|
<span>原汁原味的 Markdown 样式</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="feature-item">
|
<div className="feature-item">
|
||||||
<span className="feature-icon">🎨</span>
|
<span className="feature-icon">💡</span>
|
||||||
<span>代码语法高亮</span>
|
<span>深色界面,夜间更护眼</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="feature-item">
|
<div className="feature-item">
|
||||||
<span className="feature-icon">📊</span>
|
<span className="feature-icon">⚡</span>
|
||||||
<span>数学公式与图表</span>
|
<span>代码高亮与复制一键搞定</span>
|
||||||
</div>
|
|
||||||
<div className="feature-item">
|
|
||||||
<span className="feature-icon">📱</span>
|
|
||||||
<span>移动端友好布局</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,16 +201,15 @@ export default function MarkdownRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent);
|
const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent);
|
||||||
const hasHeadings = headings.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={content-area }>
|
<div className={contentAreaClass}>
|
||||||
<div className="content-header">
|
<div className="content-header">
|
||||||
<Breadcrumbs filePath={currentFile} />
|
<Breadcrumbs filePath={currentFile} />
|
||||||
<h1 className="content-title">{fileTitle}</h1>
|
<h1 className="content-title">{fileTitle}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={content-body }>
|
<div className="content-body">
|
||||||
<div className="markdown-pane">
|
<div className="markdown-pane">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="loading-content">
|
<div className="loading-content">
|
||||||
@@ -332,8 +228,6 @@ export default function MarkdownRenderer() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasHeadings && <TableOfContents headings={headings} />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(236, 251, 242, 0.92) 100%);
|
background: var(--color-surface-alt);
|
||||||
border-right: 1px solid var(--color-border);
|
border-right: 1px solid var(--color-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1.6rem 1.2rem 1.75rem;
|
padding: 1.5rem 1.25rem 1.75rem;
|
||||||
position: relative;
|
color: var(--color-text);
|
||||||
z-index: 1002;
|
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease, width 0.3s ease, padding 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.closed {
|
.sidebar.closed {
|
||||||
width: 0;
|
width: 0;
|
||||||
min-width: 0;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -24,35 +21,32 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header h2 {
|
.sidebar-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
color: var(--color-text);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-button {
|
.toggle-button {
|
||||||
background: rgba(51, 197, 122, 0.16);
|
background: var(--color-surface);
|
||||||
color: var(--color-text);
|
color: var(--color-muted);
|
||||||
border: 1px solid rgba(47, 177, 112, 0.18);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 999px;
|
border-radius: 12px;
|
||||||
padding: 0.45rem 0.75rem;
|
padding: 0.4rem 0.8rem;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 12px 24px rgba(86, 105, 141, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-button:hover {
|
.toggle-button:hover {
|
||||||
background: rgba(51, 197, 122, 0.28);
|
color: var(--color-accent);
|
||||||
transform: translateY(-1px);
|
border-color: rgba(58, 122, 254, 0.6);
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
@@ -61,40 +55,10 @@
|
|||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading,
|
|
||||||
.error {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 2.5rem 1rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 2px solid rgba(51, 197, 122, 0.18);
|
|
||||||
border-top: 2px solid var(--color-accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.directory-tree {
|
.directory-tree {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.3rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
@@ -106,45 +70,73 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
padding: 0.5rem 0.65rem;
|
padding: 0.5rem 0.65rem;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
|
cursor: pointer;
|
||||||
transition: background 0.2s ease, color 0.2s ease;
|
transition: background 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-content:hover {
|
.tree-node-content:hover {
|
||||||
background: rgba(47, 177, 112, 0.12);
|
background: var(--color-accent-soft);
|
||||||
color: var(--color-text);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-content.selected {
|
.tree-node-content.selected {
|
||||||
background: rgba(51, 197, 122, 0.25);
|
background: rgba(58, 122, 254, 0.16);
|
||||||
color: var(--color-text);
|
color: var(--color-accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-icon {
|
.tree-node-icon {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: transform 0.3s ease;
|
opacity: 0.9;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-content.selected .tree-node-icon {
|
.tree-node-content.selected .tree-node-icon {
|
||||||
transform: scale(1.05);
|
transform: scale(1.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-children {
|
.tree-node-children {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
padding: 2.5rem 1rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid rgba(111, 123, 146, 0.25);
|
||||||
|
border-top: 2px solid var(--color-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-backdrop {
|
.sidebar-backdrop {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(18, 43, 31, 0.18);
|
background: rgba(16, 23, 35, 0.35);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(4px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.25s ease;
|
||||||
z-index: 1001;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-backdrop.active {
|
.sidebar-backdrop.active {
|
||||||
@@ -154,49 +146,38 @@
|
|||||||
|
|
||||||
.sidebar-toggle {
|
.sidebar-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: clamp(1rem, 5vw, 1.5rem);
|
top: 1.25rem;
|
||||||
left: clamp(1rem, 5vw, 1.5rem);
|
left: 1.25rem;
|
||||||
z-index: 1200;
|
z-index: 120;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle .toggle-button {
|
.sidebar-toggle .toggle-button {
|
||||||
width: 44px;
|
padding: 0.45rem 0.9rem;
|
||||||
height: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
box-shadow: 0 16px 32px rgba(31, 58, 44, 0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle .toggle-button.open {
|
.toggle-button.open {
|
||||||
background: rgba(47, 177, 112, 0.22);
|
color: var(--color-accent);
|
||||||
}
|
border-color: rgba(58, 122, 254, 0.55);
|
||||||
|
|
||||||
.sidebar.closed ~ .sidebar-toggle .toggle-button {
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: clamp(0.75rem, 3vw, 1.5rem);
|
top: clamp(1rem, 5vw, 2rem);
|
||||||
left: clamp(0.75rem, 3vw, 1.5rem);
|
left: clamp(1rem, 5vw, 2rem);
|
||||||
height: calc(100% - clamp(1.5rem, 6vw, 3rem));
|
height: calc(100% - clamp(2rem, 10vw, 4rem));
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
transform: translateX(calc(-100% - 2.5rem));
|
transform: translateX(calc(-100% - 2.5rem));
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.open {
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transform: translateX(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,13 +192,5 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.sidebar.closed {
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
padding: 1.6rem 1.2rem 1.75rem;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ function TreeNode({ node, level = 0 }) {
|
|||||||
|
|
||||||
const isSelected = currentFile === node.path;
|
const isSelected = currentFile === node.path;
|
||||||
const hasChildren = node.children && node.children.length > 0;
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
|
const paddingLeft = `${level * 18 + 14}px`;
|
||||||
|
const contentClass = isSelected ? 'tree-node-content selected' : 'tree-node-content';
|
||||||
|
const folderIconClass = `tree-node-icon${node.isExpanded ? ' expanded' : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tree-node">
|
<div className="tree-node">
|
||||||
<div
|
<div
|
||||||
className={ ree-node-content }
|
className={contentClass}
|
||||||
style={{ paddingLeft: ${level * 18 + 14}px }}
|
style={{ paddingLeft }}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -33,7 +36,7 @@ function TreeNode({ node, level = 0 }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{node.type === NODE_TYPES.FOLDER && (
|
{node.type === NODE_TYPES.FOLDER && (
|
||||||
<span className={ ree-node-icon } aria-hidden>
|
<span className={folderIconClass} aria-hidden>
|
||||||
{hasChildren ? (node.isExpanded ? '📂' : '📁') : '📁'}
|
{hasChildren ? (node.isExpanded ? '📂' : '📁') : '📁'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -58,10 +61,13 @@ export default function Sidebar() {
|
|||||||
const { directoryTree, isLoading, error, sidebarOpen, toggleSidebar } = useApp();
|
const { directoryTree, isLoading, error, sidebarOpen, toggleSidebar } = useApp();
|
||||||
|
|
||||||
const toggleLabel = sidebarOpen ? '收起目录' : '展开目录';
|
const toggleLabel = sidebarOpen ? '收起目录' : '展开目录';
|
||||||
|
const sidebarClass = sidebarOpen ? 'sidebar open' : 'sidebar closed';
|
||||||
|
const toggleButtonClass = sidebarOpen ? 'toggle-button open' : 'toggle-button';
|
||||||
|
const backdropClass = sidebarOpen ? 'sidebar-backdrop active' : 'sidebar-backdrop';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return () => undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScrollLock = () => {
|
const handleScrollLock = () => {
|
||||||
@@ -80,7 +86,7 @@ export default function Sidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside className={sidebar } aria-hidden={!sidebarOpen}>
|
<aside className={sidebarClass} aria-hidden={!sidebarOpen}>
|
||||||
<div className="sidebar-header">
|
<div className="sidebar-header">
|
||||||
<h2>📚 萌芽笔记</h2>
|
<h2>📚 萌芽笔记</h2>
|
||||||
<button type="button" onClick={toggleSidebar} className="toggle-button" aria-label={toggleLabel}>
|
<button type="button" onClick={toggleSidebar} className="toggle-button" aria-label={toggleLabel}>
|
||||||
@@ -113,7 +119,7 @@ export default function Sidebar() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={sidebar-backdrop }
|
className={backdropClass}
|
||||||
onClick={sidebarOpen ? toggleSidebar : undefined}
|
onClick={sidebarOpen ? toggleSidebar : undefined}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@@ -122,7 +128,7 @@ export default function Sidebar() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className={ oggle-button }
|
className={toggleButtonClass}
|
||||||
aria-label={toggleLabel}
|
aria-label={toggleLabel}
|
||||||
>
|
>
|
||||||
{sidebarOpen ? '✕' : '☰'}
|
{sidebarOpen ? '✕' : '☰'}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"totalFiles": 125,
|
"totalFiles": 125,
|
||||||
"totalFolders": 50,
|
"totalFolders": 50,
|
||||||
"generatedAt": "2025-09-28T13:25:29.475Z",
|
"generatedAt": "2025-09-29T05:49:17.078Z",
|
||||||
"sourceDirectory": "C:\\Users\\BigTang\\Desktop\\Markdown转网页\\markdown-to-web\\public\\mengyanote"
|
"sourceDirectory": "E:\\React\\markdown-to-web\\public\\mengyanote"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -8,13 +8,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-link);
|
color: var(--color-accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--color-link-hover);
|
color: #1f5ce6;
|
||||||
}
|
}
|
||||||
|
|
||||||
img,
|
img,
|
||||||
|
|||||||
Reference in New Issue
Block a user