diff --git a/.github/workflows/build-gh-pages.yml b/.github/workflows/build-gh-pages.yml index 1b85be0..e4d98ec 100644 --- a/.github/workflows/build-gh-pages.yml +++ b/.github/workflows/build-gh-pages.yml @@ -2,11 +2,11 @@ name: build-gh-pages on: pull_request: - branches: [dev, main] + branches: [dev, main, gh-pages] paths-ignore: - "src/**" push: - branches: [dev, main] + branches: [dev, main, gh-pages] paths-ignore: - "src/**" diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 3fb2017..6b2db02 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -2,7 +2,7 @@ name: Deploy Jekyll and Avalonia WASM demo on: push: - branches: ["main"] + branches: ["main", "gh-pages"] paths-ignore: - "src/**" @@ -66,7 +66,7 @@ jobs: - name: Checkout demo repository uses: actions/checkout@v4 with: - repository: eesast/dotnet-workshop-solution + repository: ${{ vars.DOTNET_WORKSHOP_SOLUTION_REPOSITORY }} token: ${{ secrets.DOTNET_WORKSHOP_SOLUTION_READ }} path: dotnet-workshop-solution persist-credentials: false diff --git a/_data/navigation.yml b/_data/navigation.yml new file mode 100644 index 0000000..81b08fc --- /dev/null +++ b/_data/navigation.yml @@ -0,0 +1,46 @@ +label: 文档导航 +home_label: 项目主页 +open_label: 展开导航 +close_label: 隐藏导航 +home_url: / +sections: + - title: "`00-prepare`" + items: + - title: Guidance + url: /docs/00-prepare/guidance.html + - title: Tasks + url: /docs/00-prepare/tasks.html + - title: "`01-basic`" + items: + - title: Guidance + url: /docs/01-basic/guidance.html + - title: Tasks + url: /docs/01-basic/tasks.html + - title: "`02-multithreading`" + items: + - title: Guidance + url: /docs/02-multithreading/guidance.html + - title: Tasks + url: /docs/02-multithreading/tasks.html + - title: "`03-async-grpc`" + items: + - title: Guidance + url: /docs/03-async-grpc/guidance.html + - title: Tasks + url: /docs/03-async-grpc/tasks.html + - title: "`04-avalonia`" + items: + - title: Guidance + url: /docs/04-avalonia/guidance.html + - title: Tasks + url: /docs/04-avalonia/tasks.html + - title: "`05-advanced`" + items: + - title: Guidance + url: /docs/05-advanced/guidance.html + - title: Tasks + url: /docs/05-advanced/tasks.html + - title: "`appendix`" + items: + - title: 附录 A:部分名词不同语言对照表 + url: /docs/appendix/appendix-a-glossary.html diff --git a/_includes/sidebar.html b/_includes/sidebar.html new file mode 100644 index 0000000..5f1efff --- /dev/null +++ b/_includes/sidebar.html @@ -0,0 +1,58 @@ + + + + + diff --git a/_layouts/default.html b/_layouts/default.html index 2c179a6..9a7cab1 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -17,6 +17,8 @@ src="{{ '/assets/js/prepare.js?v=' | append: site.github.build_revision | relative_url }}"> + {% include head-custom.html %} @@ -79,7 +81,16 @@

{{ site.description | default: site.github.project_tagline }}

-
+ {% assign page_path_parts = page.url | split: '/' %} + {% assign show_sidebar = false %} + {% if page_path_parts[1] == 'docs' and page.sidebar != false %} + {% assign show_sidebar = true %} + {% endif %} + +
+ {% if show_sidebar %} + {% include sidebar.html navigation=site.data.navigation %} + {% endif %}
{{ content }}
diff --git a/assets/css/custom.css b/assets/css/custom.css index 9e2261e..f27b73e 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -35,6 +35,100 @@ code { display: inline-block; } +.content-layout.has-sidebar { + width: 94%; + max-width: 1400px; + display: grid; + grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); + gap: 40px; + align-items: start; +} + +.content-layout.has-sidebar.sidebar-collapsed { + grid-template-columns: 52px minmax(0, 1fr); +} + +.sidebar-open-button { + display: none; + padding: 10px 12px; +} + +.sidebar-collapsed .sidebar-open-button { + position: sticky; + top: 20px; + display: flex; + width: 44px; + height: 44px; + align-items: center; + justify-content: center; +} + +.sidebar-open-label { + display: none; +} + +.sidebar { + position: sticky; + top: 20px; + max-height: calc(100vh - 40px); + overflow-y: auto; + padding: 16px; +} + +.sidebar-collapsed .sidebar { + display: none; +} + +.sidebar-close-button { + display: flex; + width: 100%; + align-items: center; + gap: 8px; + margin-bottom: 16px; + padding: 8px 10px; + text-align: left; +} + +.sidebar-backdrop { + display: none; +} + +.sidebar-home { + display: block; + margin-bottom: 16px; +} + +.sidebar-section { + margin-bottom: 20px; +} + +.sidebar-section:last-child { + margin-bottom: 0; +} + +.sidebar-section summary { + padding-bottom: 6px; +} + +.sidebar-section[open] summary { + margin-bottom: 8px; +} + +.sidebar-section ul { + margin: 0; + padding-left: 5px; +} + +.sidebar-section li { + margin: 6px 0; +} + +.sidebar .active { + display: block; + margin-left: -8px; + padding: 4px 8px; +} + @media screen and (max-width: 768px) { .head_wrapper { display: flex; @@ -49,4 +143,66 @@ code { align-items: center; justify-content: center; } + + .content-layout.has-sidebar { + width: 90%; + display: block; + } + + .content-layout.has-sidebar.sidebar-collapsed { + display: block; + } + + .sidebar-open-button, + .sidebar-collapsed .sidebar-open-button { + position: fixed; + left: 16px; + bottom: 16px; + z-index: 1001; + display: flex; + width: auto; + height: auto; + align-items: center; + gap: 8px; + } + + .sidebar-open-label { + display: inline; + } + + .sidebar-mobile-open .sidebar-open-button { + display: none; + } + + .sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 1002; + display: block; + width: min(86vw, 340px); + max-height: none; + overflow-y: auto; + box-sizing: border-box; + transform: translateX(-100%); + visibility: hidden; + transition: transform 180ms ease, visibility 180ms ease; + } + + .sidebar-mobile-open .sidebar { + transform: translateX(0); + visibility: visible; + } + + .sidebar-mobile-open .sidebar-backdrop { + position: fixed; + inset: 0; + z-index: 1000; + display: block; + } + + body.sidebar-scroll-locked { + overflow: hidden; + } } diff --git a/assets/css/style.css b/assets/css/style.css index ca8f3bb..ebfaca0 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -483,3 +483,107 @@ a { .toc a { text-decoration: none; } + +/* + * Sidebar appearance + */ + +.sidebar { + border: 1px solid #30363d; + border-radius: 6px; + background: #010409; + scrollbar-width: thin; + scrollbar-color: #30363d #010409; +} + +.sidebar::-webkit-scrollbar { + width: 10px; +} + +.sidebar::-webkit-scrollbar-track { + background: #010409; + border-radius: 0 6px 6px 0; +} + +.sidebar::-webkit-scrollbar-thumb { + border: 2px solid #010409; + border-radius: 999px; + background: #30363d; +} + +.sidebar::-webkit-scrollbar-thumb:hover { + background: #484f58; +} + +.sidebar-open-button, +.sidebar-close-button { + border: 1px solid #30363d; + border-radius: 6px; + background: #161b22; + color: #f0f6fc; + font: inherit; + cursor: pointer; +} + +.sidebar-open-button:hover, +.sidebar-close-button:hover { + border-color: #58a6ff; + color: #79c0ff; +} + +.sidebar-open-button:active, +.sidebar-close-button:active { + border-color: #58a6ff; + background: #1f2937; + color: #a5d6ff; +} + +.sidebar-open-button:focus-visible, +.sidebar-close-button:focus-visible { + outline: 2px solid #4493f8; + outline-offset: 2px; +} + +.sidebar-backdrop { + border: 0; + background: rgba(0, 0, 0, 0.65); +} + +.sidebar-home { + font-weight: bold; +} + +.sidebar-section summary { + border-bottom: 1px solid #30363d; + color: #b5e853; + font-size: 1rem; + font-weight: bold; + cursor: pointer; +} + +.sidebar-section summary:hover { + color: #c8f169; +} + +.sidebar-section li { + list-style: none; +} + +.sidebar a { + text-decoration: none; +} + +.sidebar .active { + border-left: 3px solid #8b949e; + border-radius: 0 4px 4px 0; + background: rgba(110, 118, 129, 0.18); + color: #f0f6fc; + font-weight: bold; +} + +@media screen and (max-width: 768px) { + .sidebar { + border-radius: 0 6px 6px 0; + box-shadow: 8px 0 24px rgba(0, 0, 0, 0.45); + } +} diff --git a/assets/js/sidebar.js b/assets/js/sidebar.js new file mode 100644 index 0000000..508ab26 --- /dev/null +++ b/assets/js/sidebar.js @@ -0,0 +1,45 @@ +document.addEventListener('DOMContentLoaded', () => { + const layout = document.querySelector('.content-layout.has-sidebar'); + if (!layout) return; + + const sidebar = layout.querySelector('.sidebar'); + const openButton = layout.querySelector('.sidebar-open-button'); + const closeButton = layout.querySelector('.sidebar-close-button'); + const backdrop = layout.querySelector('.sidebar-backdrop'); + const narrowWindow = window.matchMedia('(max-width: 768px)'); + + const setSidebarOpen = (isOpen, shouldMoveFocus = true) => { + const isNarrow = narrowWindow.matches; + + layout.classList.toggle('sidebar-collapsed', !isNarrow && !isOpen); + layout.classList.toggle('sidebar-mobile-open', isNarrow && isOpen); + document.body.classList.toggle('sidebar-scroll-locked', isNarrow && isOpen); + + openButton.setAttribute('aria-expanded', String(isOpen)); + closeButton.setAttribute('aria-expanded', String(isOpen)); + sidebar.setAttribute('aria-hidden', String(!isOpen)); + + if (shouldMoveFocus && isOpen) { + closeButton.focus(); + } else if (shouldMoveFocus && !isOpen) { + openButton.focus(); + } + }; + + openButton.addEventListener('click', () => setSidebarOpen(true)); + closeButton.addEventListener('click', () => setSidebarOpen(false)); + backdrop.addEventListener('click', () => setSidebarOpen(false)); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && layout.classList.contains('sidebar-mobile-open')) { + setSidebarOpen(false); + } + }); + + narrowWindow.addEventListener('change', (event) => { + setSidebarOpen(!event.matches, false); + }); + + // Wide windows start expanded; narrow windows start closed. + setSidebarOpen(!narrowWindow.matches, false); +});