修改网站markdown解析格式

This commit is contained in:
2025-09-29 15:05:43 +08:00
parent df2f1bf3dd
commit c8f40f3616
11 changed files with 482 additions and 677 deletions

129
README.md
View File

@@ -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.

View File

@@ -13,12 +13,12 @@
"state": {
"type": "markdown",
"state": {
"file": "树萌芽的小本本/目前已部署网站.md",
"file": "编程语言/Android/Linux配置安卓Gradle构建环境.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "目前已部署网站"
"title": "Linux配置安卓Gradle构建环境"
}
},
{
@@ -186,8 +186,9 @@
},
"active": "8304b0e105b08ed0",
"lastOpenFiles": [
"树萌芽的小本本/网站小技巧.md",
"编程语言/Android/安卓Gradle构建常用命令总结.md",
"树萌芽的小本本/目前已部署网站.md",
"树萌芽的小本本/网站小技巧.md",
"Docker/优秀好用的Docker镜像/FileCodeBox-文件快递柜.md",
"Docker/优秀好用的Docker镜像/Postgres数据库.md",
"Docker/优秀好用的Docker镜像/未命名.md",
@@ -207,7 +208,6 @@
"Docker/Docker镜像快速迁移.md",
"无线-HCIA 02.md",
"Linux相关/把Ubuntu镜像源切换到阿里云.md",
"编程语言/Android/安卓Gradle构建常用命令总结.md",
"临时解决方案/修改hosts方式来直连Github.md",
"临时解决方案/萌芽云剪切板.md",
"2025年9月紧急规划.md",

View File

@@ -1,7 +1,6 @@
# Android Gradle 常用命令速查(基于 `./gradlew`
**基础**
##### **基础**
- `./gradlew tasks`
列出可用的 Gradle 任务(查看当前项目能跑什么任务)。
@@ -13,7 +12,7 @@
清理构建产物(删除 `build/` 目录)。
**构建 APK / AAB**
##### **构建 APK / AAB**
- `./gradlew assembleDebug`
构建 debug APK输出`app/build/outputs/apk/debug/*.apk`)。
@@ -28,7 +27,7 @@
生成 debug bundle少用通常用于测试
**按 module / productFlavor / buildType 构建**
##### **按 module / productFlavor / buildType 构建**
- `./gradlew :moduleName:assembleRelease`
构建指定 module多模块项目时用
@@ -37,7 +36,7 @@
构建指定 flavor + buildType例如 `assemblePaidRelease`)。
**安装与卸载**
##### **安装与卸载**
- `./gradlew installDebug`
将 debug APK 安装到连接的设备/模拟器(需要 adb 可用)。
@@ -48,14 +47,14 @@
- 如果用生成的 APK 手动安装:`adb install -r app/build/outputs/apk/debug/app-debug.apk`
**测试**
##### **测试**
- 单元测试JVM`./gradlew test``./gradlew testDebugUnitTest`
- 仪器/设备测试connected devices`./gradlew connectedAndroidTest``./gradlew connectedCheck`
**静态检查 / 报表**
##### **静态检查 / 报表**
- `./gradlew lint``./gradlew lintDebug`
运行 Android Lint。
@@ -64,7 +63,7 @@
输出签名信息SHA1/SHA256常用于配置 API keyGoogle/Firebase
**调试构建问题的常用参数**
##### **调试构建问题的常用参数**
- `--stacktrace` / `--full-stacktrace`:打印堆栈跟踪(排错用)
@@ -83,7 +82,7 @@
- `--refresh-dependencies`:刷新依赖缓存
**性能 / CI 常用组合示例**
##### **性能 / CI 常用组合示例**
- 本地快速一把:`./gradlew clean assembleDebug --parallel --info`
@@ -92,7 +91,7 @@
- 只构建 moduleA 的 release`./gradlew :moduleA:assembleRelease`
**常见路径**
##### **常见路径**
- APK`app/build/outputs/apk/<buildType|flavor>/...`
@@ -101,13 +100,10 @@
- 临时构建缓存:`~/.gradle/caches/`
**小贴士**
##### **小贴士**
- 始终用项目里的 Gradle Wrapper`./gradlew`),保证 Gradle 版本一致。
- Release 构建需要正确的 `signingConfig`(通常放在 `gradle.properties` + `build.gradle`),也可以在 CI 用 `-P` 传参数(注意不要把敏感信息放在日志里)。
- 出问题先加 `--stacktrace --info` 看详情再定位是依赖、ProGuard/R8、签名还是资源冲突。
如果你想,我可以把你常用的几条命令做成一个可复用的脚本/CI job 模板GitHub Actions/GitLab CI或者按你项目的 flavor 给出精确的 assemble 命令。要哪个直接说。

View File

@@ -1,19 +1,17 @@
:root {
--color-bg: #eafbf2;
--color-bg-secondary: #d6f3e3;
--color-surface: rgba(255, 255, 255, 0.88);
--color-surface-strong: #ffffff;
--color-border: rgba(47, 177, 112, 0.2);
--color-text: #1f3a2c;
--color-muted: #4a6c5a;
--color-link: #269c66;
--color-link-hover: #1e7a51;
--color-accent: #33c57a;
--color-accent-contrast: #0f3322;
--shadow-soft: 0 18px 38px rgba(36, 118, 74, 0.18);
--radius-lg: 24px;
--sidebar-width: clamp(240px, 21vw, 320px);
--gradient-bg: linear-gradient(135deg, #f2fff8 0%, #c9f4df 40%, #a6e7d0 100%);
--color-bg: #f5f7fb;
--color-surface: #ffffff;
--color-surface-alt: #f1f4fa;
--color-border: #d8deed;
--color-text: #1f2a44;
--color-muted: #6f7b92;
--color-accent: #3a7afe;
--color-accent-soft: rgba(58, 122, 254, 0.1);
--color-danger: #d6455d;
--sidebar-width: clamp(250px, 20vw, 320px);
--shadow-soft: 0 20px 45px rgba(86, 105, 141, 0.15);
--radius-lg: 22px;
--font-family-base: 'Inter', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
* {
@@ -25,32 +23,32 @@ body {
margin: 0;
padding: 0;
min-height: 100vh;
background: var(--gradient-bg);
background: linear-gradient(135deg, #f6f9ff 0%, #edf2fb 45%, #fbfdff 100%);
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 {
min-height: 100vh;
display: flex;
padding: clamp(0.75rem, 2vw, 2rem);
padding: clamp(1rem, 4vw, 2.5rem);
}
.app {
flex: 1;
display: flex;
min-height: calc(100vh - clamp(1.5rem, 4vw, 4rem));
min-height: calc(100vh - clamp(2rem, 6vw, 5rem));
border-radius: var(--radius-lg);
background: var(--color-surface);
backdrop-filter: blur(16px);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-soft);
overflow: hidden;
position: relative;
}
body ::selection {
background-color: rgba(51, 197, 122, 0.35);
color: var(--color-accent-contrast);
background-color: rgba(58, 122, 254, 0.18);
color: var(--color-text);
}
::-webkit-scrollbar {
@@ -58,38 +56,39 @@ body ::selection {
}
::-webkit-scrollbar-track {
background: transparent;
background: rgba(216, 222, 237, 0.4);
border-radius: 999px;
}
::-webkit-scrollbar-thumb {
background: rgba(43, 156, 105, 0.25);
border-radius: 9999px;
background: rgba(111, 123, 146, 0.35);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(43, 156, 105, 0.4);
background: rgba(111, 123, 146, 0.55);
}
button:focus,
a:focus {
outline: 3px solid rgba(51, 197, 122, 0.45);
outline: 2px solid rgba(58, 122, 254, 0.45);
outline-offset: 2px;
}
@media (max-width: 1024px) {
#root {
padding: clamp(0.5rem, 3vw, 1.25rem);
padding: clamp(0.75rem, 4vw, 1.75rem);
}
.app {
min-height: calc(100vh - clamp(1rem, 6vw, 3rem));
min-height: calc(100vh - clamp(1.5rem, 7vw, 3.5rem));
border-radius: 18px;
}
}
@media (max-width: 768px) {
#root {
padding: clamp(0.5rem, 4vw, 1rem);
padding: clamp(0.5rem, 5vw, 1.2rem);
}
.app {

View File

@@ -2,172 +2,68 @@
flex: 1;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(233, 250, 240, 0.92) 100%);
overflow-y: auto;
position: relative;
background: var(--color-surface);
color: var(--color-text);
}
.content-area.with-sidebar {
border-left: 1px solid rgba(47, 177, 112, 0.12);
border-left: 1px solid var(--color-border);
}
.content-header {
position: sticky;
top: 0;
z-index: 100;
padding: clamp(1.25rem, 3vw, 2rem) clamp(1.25rem, 4vw, 2.5rem) clamp(0.75rem, 2vw, 1.25rem);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(47, 177, 112, 0.12);
z-index: 10;
padding: 1.75rem clamp(2rem, 5vw, 3rem) 1.25rem;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
.breadcrumbs {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
gap: 0.45rem;
font-size: 0.85rem;
color: var(--color-muted);
margin-bottom: 0.75rem;
margin-bottom: 0.8rem;
}
.breadcrumb-item {
display: inline-flex;
align-items: center;
gap: 0.35rem;
gap: 0.4rem;
}
.breadcrumb-separator {
color: rgba(47, 177, 112, 0.35);
color: rgba(31, 42, 68, 0.3);
}
.breadcrumb-text {
padding: 0.25rem 0.55rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(51, 197, 122, 0.12);
transition: background 0.2s ease, color 0.2s ease;
}
.breadcrumb-text:hover {
background: rgba(51, 197, 122, 0.22);
color: var(--color-text);
background: rgba(58, 122, 254, 0.08);
}
.content-title {
margin: 0;
font-size: clamp(1.8rem, 3vw, 2.4rem);
font-weight: 700;
font-size: clamp(2rem, 3.5vw, 2.6rem);
font-weight: 600;
color: var(--color-text);
letter-spacing: 0.01em;
}
.content-body {
padding: clamp(1.5rem, 4vw, 3rem) clamp(1.25rem, 6vw, 3.5rem) clamp(2.5rem, 6vw, 4rem);
width: 100%;
max-width: 1200px;
margin: 0 auto;
display: grid;
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);
flex: 1;
overflow-y: auto;
display: flex;
justify-content: center;
padding: clamp(2rem, 6vw, 3.5rem);
}
.markdown-pane {
min-width: 0;
}
.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);
}
width: min(860px, 100%);
}
.markdown-content {
color: var(--color-text);
line-height: 1.75;
font-size: 1rem;
}
@@ -182,31 +78,31 @@
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin: 2.2rem 0 1rem;
font-weight: 700;
line-height: 1.25;
margin: 2rem 0 1rem;
font-weight: 600;
color: var(--color-text);
position: relative;
}
.markdown-content h1 {
font-size: clamp(2rem, 3vw, 2.6rem);
border-bottom: 2px solid rgba(47, 177, 112, 0.14);
padding-bottom: 0.75rem;
font-size: clamp(2.1rem, 3vw, 2.8rem);
border-bottom: 2px solid rgba(31, 42, 68, 0.08);
padding-bottom: 0.6rem;
}
.markdown-content h2 {
font-size: clamp(1.6rem, 2.4vw, 2.1rem);
border-bottom: 1px solid rgba(47, 177, 112, 0.16);
padding-bottom: 0.5rem;
font-size: clamp(1.7rem, 2.5vw, 2.2rem);
border-bottom: 1px solid rgba(31, 42, 68, 0.08);
padding-bottom: 0.45rem;
}
.markdown-content h3 {
font-size: clamp(1.35rem, 2vw, 1.6rem);
font-size: clamp(1.4rem, 2vw, 1.7rem);
}
.markdown-content h4 {
font-size: 1.2rem;
font-size: 1.15rem;
letter-spacing: 0.01em;
}
.heading-anchor {
@@ -214,16 +110,16 @@
left: -1.5rem;
top: 50%;
transform: translateY(-50%);
opacity: 0;
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;
}
.markdown-content h1:hover .heading-anchor,
.markdown-content h2:hover .heading-anchor,
.markdown-content h3:hover .heading-anchor,
.markdown-content h4:hover .heading-anchor,
.markdown-content h1:hover .heading-anchor {
.markdown-content h4:hover .heading-anchor {
opacity: 1;
transform: translate(-4px, -50%);
}
@@ -232,10 +128,9 @@
.markdown-content ul,
.markdown-content ol,
.markdown-content blockquote,
.markdown-content table,
.markdown-content pre {
margin-top: 0;
margin-bottom: 1.35rem;
.markdown-content pre,
.markdown-content table {
margin: 0 0 1.35rem;
}
.markdown-content ul,
@@ -248,32 +143,27 @@
}
.markdown-content a {
color: var(--color-link);
color: var(--color-accent);
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
.markdown-content a:hover {
color: var(--color-link-hover);
text-underline-offset: 3px;
}
.inline-code {
display: inline-flex;
align-items: center;
padding: 0.1rem 0.35rem;
padding: 0.15rem 0.4rem;
border-radius: 6px;
background: rgba(33, 169, 102, 0.12);
border: 1px solid rgba(33, 169, 102, 0.18);
background: rgba(58, 122, 254, 0.08);
border: 1px solid rgba(58, 122, 254, 0.16);
color: var(--color-text);
font-size: 0.95em;
}
.code-block-wrapper {
position: relative;
background: #0f2c1f;
border-radius: 16px;
box-shadow: 0 18px 36px rgba(12, 52, 34, 0.45);
background: #f6f8ff;
border: 1px solid rgba(58, 122, 254, 0.15);
border-radius: 14px;
overflow: hidden;
}
@@ -282,75 +172,99 @@
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
color: #bcf8d9;
text-transform: uppercase;
background: rgba(58, 122, 254, 0.08);
color: var(--color-muted);
font-size: 0.75rem;
letter-spacing: 0.12em;
}
.code-language {
letter-spacing: 0.1em;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.code-copy-button {
background: rgba(188, 248, 217, 0.14);
color: #bcf8d9;
border: 1px solid rgba(188, 248, 217, 0.25);
border-radius: 999px;
padding: 0.35rem 0.75rem;
font-size: 0.75rem;
border: 1px solid rgba(58, 122, 254, 0.25);
background: #fff;
color: var(--color-muted);
border-radius: 8px;
padding: 0.35rem 0.7rem;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
transition: all 0.2s ease;
}
.code-copy-button:hover {
background: rgba(188, 248, 217, 0.24);
transform: translateY(-1px);
color: var(--color-accent);
border-color: rgba(58, 122, 254, 0.5);
}
.code-copy-button.copied {
background: rgba(188, 248, 217, 0.4);
color: #0f2c1f;
color: var(--color-accent);
border-color: rgba(58, 122, 254, 0.7);
}
pre {
margin: 0;
padding: 1.25rem 1.5rem;
padding: 1.1rem 1.35rem;
overflow-x: auto;
font-size: 0.95rem;
}
pre code {
background: transparent !important;
color: #e9fff2;
font-size: 0.95rem;
color: #1f2a44;
}
.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 {
width: 100%;
overflow-x: auto;
border-radius: 16px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(47, 177, 112, 0.14);
box-shadow: 0 14px 28px rgba(31, 58, 44, 0.12);
border-radius: 14px;
border: 1px solid rgba(31, 42, 68, 0.12);
background: #ffffff;
}
.table-wrapper table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
}
.table-wrapper th,
.table-wrapper td {
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;
}
.table-wrapper th {
background: rgba(51, 197, 122, 0.12);
color: var(--color-text);
color: var(--color-muted);
font-weight: 600;
}
@@ -358,205 +272,117 @@ pre code {
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 {
display: inline-flex;
align-items: center;
gap: 0.25rem;
gap: 0.3rem;
}
.external-link-icon {
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 {
display: flex;
align-items: center;
justify-content: center;
padding: clamp(2rem, 6vw, 4rem);
min-height: calc(100vh - clamp(10rem, 16vw, 12rem));
min-height: calc(100vh - 7rem);
}
.welcome-content {
text-align: center;
max-width: 640px;
padding: clamp(2rem, 6vw, 3rem);
border-radius: 28px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 28px 48px rgba(31, 58, 44, 0.16);
max-width: 520px;
padding: 2.4rem;
border-radius: 18px;
background: #ffffff;
border: 1px solid var(--color-border);
box-shadow: 0 24px 48px rgba(86, 105, 141, 0.18);
}
.welcome-content h1 {
font-size: clamp(2.4rem, 4vw, 3rem);
color: var(--color-text);
margin-bottom: 1rem;
margin-bottom: 0.9rem;
font-size: clamp(2.1rem, 3.2vw, 2.6rem);
}
.welcome-content p {
font-size: 1.1rem;
margin: 0 auto 1.6rem;
color: var(--color-muted);
margin-bottom: 2rem;
}
.welcome-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1.1rem;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.8rem;
}
.feature-item {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 1rem;
background: rgba(51, 197, 122, 0.12);
border-radius: 16px;
border: 1px solid rgba(47, 177, 112, 0.14);
color: var(--color-text);
gap: 0.6rem;
padding: 0.85rem;
border: 1px solid var(--color-border);
border-radius: 12px;
background: var(--color-surface);
color: var(--color-muted);
}
.feature-icon {
font-size: 1.5rem;
}
.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;
}
font-size: 1.1rem;
}
@media (max-width: 768px) {
.content-header {
padding: 1rem 1.25rem;
padding: 1.2rem 1.5rem 0.9rem;
}
.content-body {
padding: 1.25rem;
padding: 1.5rem;
}
.content-title {
font-size: clamp(1.6rem, 6vw, 2rem);
}
.markdown-content {
font-size: 0.98rem;
.markdown-pane {
width: 100%;
}
.heading-anchor {
display: none;
}
.welcome-content {
padding: 2rem;
}
.welcome-features {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.content-header {
padding: 0.85rem 1rem;
padding: 1rem 1.1rem 0.75rem;
}
.content-body {
padding: 1rem;
}
.breadcrumbs {
font-size: 0.8rem;
}
.feature-item {
flex-direction: column;
text-align: center;
}
}

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import React, { useMemo, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
@@ -15,6 +15,7 @@ import 'highlight.js/styles/github.css';
function Breadcrumbs({ filePath }) {
const breadcrumbs = generateBreadcrumbs(filePath);
if (breadcrumbs.length === 0) return null;
return (
<nav className="breadcrumbs" aria-label="当前位置">
{breadcrumbs.map((crumb, index) => (
@@ -28,8 +29,7 @@ function Breadcrumbs({ filePath }) {
}
function CodeBlock({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const [copied, setCopied] = useState(false);
if (inline) {
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 (
<div className="code-block-wrapper">
{language && (
<div className="code-block-header">
<span className="code-language">{language}</span>
</div>
)}
<div className="code-block-header">
<span className="code-language">{language}</span>
<button type="button" className={buttonClass} onClick={handleCopy} aria-live="polite">
{buttonLabel}
</button>
</div>
<pre className={className} {...props}>
<code>{children}</code>
</pre>
@@ -57,19 +78,21 @@ function CustomLink({ href, children, ...props }) {
if (href && href.startsWith('[[') && href.endsWith(']]')) {
const linkText = href.slice(2, -2);
return (
<span className="internal-link" title={内部链接: }>
<span className="internal-link" title={`内部链接: ${linkText}`}>
{children || linkText}
</span>
);
}
const isExternal = href && /^(https?:)?\/\//.test(href);
const linkClass = isExternal ? 'external-link' : 'internal-link';
return (
<a
href={href}
target={isExternal ? '_blank' : '_self'}
rel={isExternal ? 'noopener noreferrer' : undefined}
className={isExternal ? 'external-link' : 'internal-link'}
className={linkClass}
{...props}
>
{children}
@@ -86,97 +109,8 @@ function CustomTable({ children, ...props }) {
);
}
function useHeadings(content) {
return useMemo(() => {
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 }) => {
function createHeadingRenderer(tag) {
return function HeadingRenderer({ children, ...props }) {
const text = React.Children.toArray(children)
.map((child) => {
if (typeof child === 'string') return child;
@@ -190,111 +124,74 @@ export default function MarkdownRenderer() {
const id = text
.toLowerCase()
.replace(/[^a-z0-9\u00C0-\u024f\u4e00-\u9fa5\s-]/g, '')
.replace(/[^a-z0-9\u00c0-\u024f\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
const HeadingTag = tag;
return (
<Tag id={id} {...props}>
<a href={#} aria-hidden className="heading-anchor">
<HeadingTag id={id} {...props}>
<a href={`#${id}`} aria-hidden className="heading-anchor">
#
</a>
{children}
</Tag>
</HeadingTag>
);
}, []);
};
}
const components = useMemo(() => ({
code: CodeBlock,
a: CustomLink,
table: CustomTable,
h1: renderHeading('h1'),
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 headingComponents = {
h1: createHeadingRenderer('h1'),
h2: createHeadingRenderer('h2'),
h3: createHeadingRenderer('h3'),
h4: createHeadingRenderer('h4'),
};
const calloutMatch = childText.match(/^\s*\[(info|warning|danger|success)\]\s*/i);
const calloutType = calloutMatch ? calloutMatch[1].toLowerCase() : null;
export default function MarkdownRenderer() {
const { currentFile, currentContent, isLoading, sidebarOpen } = useApp();
return (
<blockquote
className={custom-blockquote}
{...props}
>
const components = useMemo(
() => ({
code: CodeBlock,
a: CustomLink,
table: CustomTable,
...headingComponents,
blockquote: ({ children, ...props }) => (
<blockquote className="custom-blockquote" {...props}>
{children}
</blockquote>
);
},
img: ({ alt, ...props }) => (
<figure className="markdown-image">
<img alt={alt} {...props} />
{alt && <figcaption>{alt}</figcaption>}
</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]);
),
img: ({ alt, ...props }) => (
<figure className="markdown-image">
<img alt={alt} {...props} />
{alt && <figcaption>{alt}</figcaption>}
</figure>
),
}),
[],
);
useEffect(() => {
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]);
const contentAreaClass = 'content-area' + (sidebarOpen ? ' with-sidebar' : '');
if (!currentFile) {
return (
<div className={content-area }>
<div className={contentAreaClass}>
<div className="welcome-message">
<div className="welcome-content">
<h1>🌱 欢迎来到萌芽笔记</h1>
<p>从左侧导航栏选择一个笔记文件开始阅读</p>
<h1>🌙 欢迎</h1>
<p>从左侧目录选择任意 Markdown 笔记即可开始阅读</p>
<div className="welcome-features">
<div className="feature-item">
<span className="feature-icon">📝</span>
<span>支持完整的Markdown语法</span>
<span>原汁原味的 Markdown 样式</span>
</div>
<div className="feature-item">
<span className="feature-icon">🎨</span>
<span>代码语法高亮</span>
<span className="feature-icon">💡</span>
<span>深色界面夜间更护眼</span>
</div>
<div className="feature-item">
<span className="feature-icon">📊</span>
<span>数学公式与图表</span>
</div>
<div className="feature-item">
<span className="feature-icon">📱</span>
<span>移动端友好布局</span>
<span className="feature-icon"></span>
<span>代码高亮与复制一键搞定</span>
</div>
</div>
</div>
@@ -304,16 +201,15 @@ export default function MarkdownRenderer() {
}
const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent);
const hasHeadings = headings.length > 0;
return (
<div className={content-area }>
<div className={contentAreaClass}>
<div className="content-header">
<Breadcrumbs filePath={currentFile} />
<h1 className="content-title">{fileTitle}</h1>
</div>
<div className={content-body }>
<div className="content-body">
<div className="markdown-pane">
{isLoading ? (
<div className="loading-content">
@@ -332,8 +228,6 @@ export default function MarkdownRenderer() {
</div>
)}
</div>
{hasHeadings && <TableOfContents headings={headings} />}
</div>
</div>
);

View File

@@ -1,18 +1,15 @@
.sidebar {
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);
display: flex;
flex-direction: column;
padding: 1.6rem 1.2rem 1.75rem;
position: relative;
z-index: 1002;
transition: transform 0.3s ease, opacity 0.3s ease, width 0.3s ease, padding 0.3s ease;
padding: 1.5rem 1.25rem 1.75rem;
color: var(--color-text);
}
.sidebar.closed {
width: 0;
min-width: 0;
padding: 0;
opacity: 0;
pointer-events: none;
@@ -24,35 +21,32 @@
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 1.5rem;
margin-bottom: 1.75rem;
}
.sidebar-header h2 {
margin: 0;
font-size: 1.2rem;
color: var(--color-text);
font-weight: 700;
font-size: 1.1rem;
letter-spacing: 0.02em;
font-weight: 600;
color: var(--color-text);
}
.toggle-button {
background: rgba(51, 197, 122, 0.16);
color: var(--color-text);
border: 1px solid rgba(47, 177, 112, 0.18);
border-radius: 999px;
padding: 0.45rem 0.75rem;
font-size: 1rem;
background: var(--color-surface);
color: var(--color-muted);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 0.4rem 0.8rem;
font-size: 0.95rem;
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 {
background: rgba(51, 197, 122, 0.28);
transform: translateY(-1px);
}
.toggle-button:active {
transform: translateY(0);
color: var(--color-accent);
border-color: rgba(58, 122, 254, 0.6);
}
.sidebar-content {
@@ -61,40 +55,10 @@
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 {
display: flex;
flex-direction: column;
gap: 0.3rem;
gap: 0.35rem;
}
.tree-node {
@@ -106,45 +70,73 @@
align-items: center;
gap: 0.55rem;
padding: 0.5rem 0.65rem;
cursor: pointer;
border-radius: 12px;
color: var(--color-muted);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.tree-node-content:hover {
background: rgba(47, 177, 112, 0.12);
color: var(--color-text);
background: var(--color-accent-soft);
color: var(--color-accent);
}
.tree-node-content.selected {
background: rgba(51, 197, 122, 0.25);
color: var(--color-text);
background: rgba(58, 122, 254, 0.16);
color: var(--color-accent);
font-weight: 600;
}
.tree-node-icon {
font-size: 1rem;
transition: transform 0.3s ease;
opacity: 0.9;
transition: transform 0.2s ease;
}
.tree-node-content.selected .tree-node-icon {
transform: scale(1.05);
transform: scale(1.06);
}
.tree-node-children {
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 {
position: absolute;
position: fixed;
inset: 0;
background: rgba(18, 43, 31, 0.18);
backdrop-filter: blur(2px);
background: rgba(16, 23, 35, 0.35);
backdrop-filter: blur(4px);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 1001;
transition: opacity 0.25s ease;
z-index: 50;
}
.sidebar-backdrop.active {
@@ -154,49 +146,38 @@
.sidebar-toggle {
position: fixed;
top: clamp(1rem, 5vw, 1.5rem);
left: clamp(1rem, 5vw, 1.5rem);
z-index: 1200;
top: 1.25rem;
left: 1.25rem;
z-index: 120;
}
.sidebar-toggle .toggle-button {
width: 44px;
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);
padding: 0.45rem 0.9rem;
}
.sidebar-toggle .toggle-button.open {
background: rgba(47, 177, 112, 0.22);
}
.sidebar.closed ~ .sidebar-toggle .toggle-button {
background: rgba(255, 255, 255, 0.92);
.toggle-button.open {
color: var(--color-accent);
border-color: rgba(58, 122, 254, 0.55);
}
@media (max-width: 1024px) {
.sidebar {
position: absolute;
top: clamp(0.75rem, 3vw, 1.5rem);
left: clamp(0.75rem, 3vw, 1.5rem);
height: calc(100% - clamp(1.5rem, 6vw, 3rem));
top: clamp(1rem, 5vw, 2rem);
left: clamp(1rem, 5vw, 2rem);
height: calc(100% - clamp(2rem, 10vw, 4rem));
border-radius: 20px;
box-shadow: var(--shadow-soft);
transform: translateX(calc(-100% - 2.5rem));
transition: transform 0.3s ease, opacity 0.3s ease;
opacity: 0;
pointer-events: none;
}
.sidebar.open {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
}
@@ -211,13 +192,5 @@
transform: none;
opacity: 1;
pointer-events: auto;
height: auto;
}
}
@media (max-width: 1024px) {
.sidebar.closed {
width: var(--sidebar-width);
padding: 1.6rem 1.2rem 1.75rem;
opacity: 0;
}
}

View File

@@ -16,12 +16,15 @@ function TreeNode({ node, level = 0 }) {
const isSelected = currentFile === node.path;
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 (
<div className="tree-node">
<div
className={ ree-node-content }
style={{ paddingLeft: ${level * 18 + 14}px }}
className={contentClass}
style={{ paddingLeft }}
onClick={handleClick}
role="button"
tabIndex={0}
@@ -33,7 +36,7 @@ function TreeNode({ node, level = 0 }) {
}}
>
{node.type === NODE_TYPES.FOLDER && (
<span className={ ree-node-icon } aria-hidden>
<span className={folderIconClass} aria-hidden>
{hasChildren ? (node.isExpanded ? '📂' : '📁') : '📁'}
</span>
)}
@@ -58,10 +61,13 @@ export default function Sidebar() {
const { directoryTree, isLoading, error, sidebarOpen, toggleSidebar } = useApp();
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(() => {
if (typeof window === 'undefined') {
return () => undefined;
return undefined;
}
const handleScrollLock = () => {
@@ -80,7 +86,7 @@ export default function Sidebar() {
return (
<>
<aside className={sidebar } aria-hidden={!sidebarOpen}>
<aside className={sidebarClass} aria-hidden={!sidebarOpen}>
<div className="sidebar-header">
<h2>📚 萌芽笔记</h2>
<button type="button" onClick={toggleSidebar} className="toggle-button" aria-label={toggleLabel}>
@@ -113,7 +119,7 @@ export default function Sidebar() {
</aside>
<div
className={sidebar-backdrop }
className={backdropClass}
onClick={sidebarOpen ? toggleSidebar : undefined}
aria-hidden="true"
/>
@@ -122,7 +128,7 @@ export default function Sidebar() {
<button
type="button"
onClick={toggleSidebar}
className={ oggle-button }
className={toggleButtonClass}
aria-label={toggleLabel}
>
{sidebarOpen ? '✕' : '☰'}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"totalFiles": 125,
"totalFolders": 50,
"generatedAt": "2025-09-28T13:25:29.475Z",
"sourceDirectory": "C:\\Users\\BigTang\\Desktop\\Markdown转网页\\markdown-to-web\\public\\mengyanote"
"generatedAt": "2025-09-29T05:49:17.078Z",
"sourceDirectory": "E:\\React\\markdown-to-web\\public\\mengyanote"
}

View File

@@ -1,6 +1,6 @@
:root {
font-size: 16px;
line-height: 1.5;
line-height: 1.6;
font-weight: 400;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@@ -8,13 +8,13 @@
}
a {
color: var(--color-link);
color: var(--color-accent);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: var(--color-link-hover);
color: #1f5ce6;
}
img,