Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions site/.vitepress/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ export default withDrawio(defineConfig({
{},
`(function(){try{var s=localStorage.getItem('vp-font-size')||'normal';if(s!=='xxsmall'&&s!=='small'&&s!=='normal'&&s!=='large'&&s!=='xxlarge'){s='normal';}document.documentElement.dataset.fontSize=s;}catch(e){}})()`,
],
// 首屏立即应用侧栏宽度(左导航 + 右大纲),防刷新闪烁。key 与 ResizableSidebar.vue 一致。
[
'script',
{},
`(function(){try{var w=parseInt(localStorage.getItem('vp-sidebar-width'));if(!w||w<200||w>480){w=272;}document.documentElement.style.setProperty('--vp-sidebar-width',w+'px');var a=parseInt(localStorage.getItem('vp-aside-width'));if(!a||a<180||a>360){a=256;}document.documentElement.style.setProperty('--vp-aside-width',a+'px');}catch(e){}})()`,
],
],

markdown: sharedMarkdown,
Expand Down
6 changes: 6 additions & 0 deletions site/.vitepress/config/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export const sharedBase = {
{},
`(function(){try{var s=localStorage.getItem('vp-font-size')||'normal';if(s!=='xxsmall'&&s!=='small'&&s!=='normal'&&s!=='large'&&s!=='xxlarge'){s='normal';}document.documentElement.dataset.fontSize=s;}catch(e){}})()`,
],
// 首屏立即应用侧栏宽度(左导航 + 右大纲),防刷新闪烁。key 与 ResizableSidebar.vue 一致。
[
'script',
{},
`(function(){try{var w=parseInt(localStorage.getItem('vp-sidebar-width'));if(!w||w<200||w>480){w=272;}document.documentElement.style.setProperty('--vp-sidebar-width',w+'px');var a=parseInt(localStorage.getItem('vp-aside-width'));if(!a||a<180||a>360){a=256;}document.documentElement.style.setProperty('--vp-aside-width',a+'px');}catch(e){}})()`,
],
],

markdown: sharedMarkdown,
Expand Down
177 changes: 177 additions & 0 deletions site/.vitepress/theme/components/ResizableSidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue'

// 可拖拽侧栏宽度:左导航树 + 右大纲栏(TOC)。
// 左栏 --vp-sidebar-width 由 VitePress 全链路消费(sidebar 自身 / 正文 padding-left / 顶栏对齐),
// 改这一个变量即全联动。但 handle 的定位与拖动计算必须读 .VPSidebar 的实际几何 —— 宽屏
// (≥1440px)布局居中,sidebar 左缘非 0、右缘带 (vw-maxW)/2 偏移,且受滚动条宽度影响,
// CSS 公式推算总有小偏差。故 left 一律用 JS 读 getBoundingClientRect 精确设定(与右 handle 同策略)。
// 右栏 --vp-aside-width 自定义变量(在 custom.css 覆盖 aside max-width);右 handle absolute
// 注入 aside 内,MutationObserver 在路由切换重建 aside 时重新注入。
// 宽度持久化 localStorage;首屏防闪由 config head 内联脚本(hydration 前注入变量)负责。

type Side = 'left' | 'right'
interface Dim { min: number; max: number; def: number; key: string; cssVar: string }

const CONF: Record<Side, Dim> = {
left: { min: 200, max: 480, def: 272, key: 'vp-sidebar-width', cssVar: '--vp-sidebar-width' },
right: { min: 180, max: 360, def: 256, key: 'vp-aside-width', cssVar: '--vp-aside-width' },
}

const leftHandle = ref<HTMLElement | null>(null)
const RIGHT_HANDLE_ID = 'rs-right-handle'

const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
const applyVar = (side: Side, px: number) =>
document.documentElement.style.setProperty(CONF[side].cssVar, px + 'px')
const persist = (side: Side, px: number) => {
try { localStorage.setItem(CONF[side].key, String(px)) } catch { /* 隐私模式 / 配额 */ }
}

interface DragCtx {
side: Side
dim: Dim
lastV: number
handle: HTMLElement
onMove: (e: MouseEvent) => void
onUp: () => void
}
let drag: DragCtx | null = null

function startDrag(side: Side, e: MouseEvent) {
e.preventDefault()
const dim = CONF[side]
const handle = e.currentTarget as HTMLElement
handle.classList.add('is-active')
document.body.classList.add('rs-resizing')

// 左栏:sidebar 左缘在居中布局下非 0,新宽 = 鼠标x − 左缘
const sidebarLeft =
side === 'left'
? (document.querySelector('.VPSidebar') as HTMLElement | null)?.getBoundingClientRect().left ?? 0
: 0

const onMove = (ev: MouseEvent) => {
let v: number
if (side === 'left') {
v = ev.clientX - sidebarLeft
} else {
// 右栏:aside 右边缘固定(贴容器右内边),新宽 = 右边缘 − 鼠标 x
const aside = document.querySelector('.VPDoc.has-aside .aside') as HTMLElement | null
v = aside ? aside.getBoundingClientRect().right - ev.clientX : dim.def
}
if (drag) drag.lastV = clamp(Math.round(v), dim.min, dim.max)
applyVar(side, drag ? drag.lastV : dim.def)
if (side === 'left' && drag?.handle) {
// handle 跟随 sidebar 右缘(左缘 + 当前宽)
drag.handle.style.left = (sidebarLeft + drag.lastV) + 'px'
}
}
const onUp = () => {
if (drag) persist(side, drag.lastV)
handle.classList.remove('is-active')
document.body.classList.remove('rs-resizing')
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onUp)
drag = null
}
drag = { side, dim, lastV: dim.def, handle, onMove, onUp }
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onUp)
}

function reset(side: Side) {
applyVar(side, CONF[side].def)
persist(side, CONF[side].def)
if (side === 'left') updateLeftPosition()
}

// 仅在有侧栏的页(文档页)显示左 handle;首页等无侧栏页隐藏
function updateLeftVisibility() {
if (!leftHandle.value) return
leftHandle.value.style.display = document.querySelector('.VPSidebar') ? '' : 'none'
}

// 左 handle 精确定位:用 offsetLeft + offsetWidth(不含 transform),避开 sidebar 入场过渡
// (translateX(-100%)→0)对 getBoundingClientRect 的干扰 —— 首屏即读到最终右边缘,无需等动画结束。
function updateLeftPosition() {
if (!leftHandle.value) return
const sb = document.querySelector('.VPSidebar') as HTMLElement | null
if (sb) leftHandle.value.style.left = (sb.offsetLeft + sb.offsetWidth) + 'px'
}

function injectRightHandle() {
const aside = document.querySelector('.VPDoc.has-aside .aside') as HTMLElement | null
if (!aside || aside.querySelector('#' + RIGHT_HANDLE_ID)) return
const handle = document.createElement('div')
handle.id = RIGHT_HANDLE_ID
handle.className = 'rs-handle rs-handle--right'
handle.setAttribute('role', 'separator')
handle.setAttribute('aria-orientation', 'vertical')
handle.setAttribute('aria-label', '拖动调整右侧大纲栏宽度(双击重置)')
handle.addEventListener('mousedown', (ev) => startDrag('right', ev))
handle.addEventListener('dblclick', () => reset('right'))
aside.style.position = 'relative'
aside.appendChild(handle)
}

let observer: MutationObserver | null = null
let leftTimer = 0
const onMutate = () => {
updateLeftVisibility()
updateLeftPosition()
injectRightHandle()
}

onMounted(() => {
// 恢复已存宽度(防闪脚本已在首屏注入,这里做内存兜底)
;(['left', 'right'] as Side[]).forEach((side) => {
const dim = CONF[side]
try {
const v = parseInt(localStorage.getItem(dim.key) || '')
if (v >= dim.min && v <= dim.max) applyVar(side, v)
} catch {}
})
onMutate()
// sidebar 桌面端有 transform 入场过渡(translateX(-100%)→0,约 0.25s),过渡中
// getBoundingClientRect 偏左,导致 handle 初始错位(拖动后才贴合)。
// 多时机补校准,确保首屏即贴合:下一帧 / 过渡结束后(~350ms)/ 页面 load 后。
requestAnimationFrame(updateLeftPosition)
leftTimer = window.setTimeout(updateLeftPosition, 350)
window.addEventListener('resize', updateLeftPosition, { passive: true })
if (document.readyState !== 'complete') {
window.addEventListener('load', updateLeftPosition)
}
const root = document.querySelector('.VPContent') || document.body
if ('MutationObserver' in window) {
observer = new MutationObserver(onMutate)
observer.observe(root, { childList: true, subtree: true })
}
})

onBeforeUnmount(() => {
observer?.disconnect()
observer = null
window.clearTimeout(leftTimer)
window.removeEventListener('resize', updateLeftPosition)
window.removeEventListener('load', updateLeftPosition)
document.querySelectorAll('#' + RIGHT_HANDLE_ID).forEach((n) => n.remove())
if (drag) {
document.removeEventListener('mousemove', drag.onMove)
document.removeEventListener('mouseup', drag.onUp)
drag = null
}
})
</script>

<template>
<div
ref="leftHandle"
class="rs-handle rs-handle--left"
role="separator"
aria-orientation="vertical"
aria-label="拖动调整左侧导航栏宽度(双击重置)"
@mousedown="startDrag('left', $event)"
@dblclick="reset('left')"
></div>
</template>
83 changes: 83 additions & 0 deletions site/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,89 @@ html.dark .online-compiler-demo__source-highlight .shiki span {
font-size: 0.85em;
}

/* ================================================================
Resizable Sidebar(可拖拽侧栏宽度)
组件: theme/components/ResizableSidebar.vue
左 handle 在组件模板内(fixed),右 handle 运行时注入 .aside(absolute)。
宽度由 --vp-sidebar-width(VitePress 原生,全链路消费)与 --vp-aside-width(本站自定义)驱动。
================================================================ */

:root {
--vp-aside-width: 256px; /* 右大纲栏宽度变量,默认与 VitePress 原值一致 */
}

/* 右栏宽度由变量驱动,覆盖 VitePress 的固定 max-width / width */
.VPDoc.has-aside .aside {
max-width: var(--vp-aside-width);
position: relative; /* 给注入的 absolute handle 提供锚点 */
}
.VPDoc .aside-container {
width: calc(var(--vp-aside-width) - 32px); /* 保持原 32px 内边距逻辑(原 max-width 256 → container 224) */
}

/* 手柄公共态:常显一条 1px 淡灰细线(::before),悬浮 / 拖拽时变品牌色加粗。
命中热区 8px(透明本体),避免过窄扫不到。z-index 压在 sidebar 之上,防止被侧栏盖住。 */
.rs-handle {
z-index: calc(var(--vp-z-index-sidebar) + 1);
width: 8px;
background: transparent;
cursor: col-resize;
}
.rs-handle::before {
content: '';
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
transform: translateX(-50%);
background: var(--vp-c-divider);
transition: width 0.15s ease, background-color 0.15s ease;
}
.rs-handle:hover::before,
.rs-handle.is-active::before {
width: 2px;
background: var(--vp-c-brand-1);
}

/* 左手柄:fixed。left 仅作 SSR 首屏 fallback,onMounted 后由 JS 读取 .VPSidebar 右边缘精确
覆盖 —— 宽屏(≥1440px)布局居中,右缘带 (vw-maxW)/2 偏移且受滚动条影响,CSS 公式推算
总有小偏差,改读 getBoundingClientRect 根治(与右 handle 同策略)。 */
.rs-handle--left {
position: fixed;
left: var(--vp-sidebar-width);
margin-left: -4px;
top: var(--vp-nav-height, 64px);
bottom: 0;
}

/* 右手柄:absolute 注入 .aside 内,居中压在其左边缘(默认隐藏,仅大屏显示见下方媒体查询) */
.rs-handle--right {
position: absolute;
left: -4px;
top: 0;
bottom: 0;
display: none;
}

/* 拖拽进行中:禁选中文,全屏 col-resize 光标 */
body.rs-resizing {
user-select: none;
cursor: col-resize;
}

/* 响应式:左栏 <960px 沉为抽屉;右栏 aside 仅 ≥1280px 显示 */
@media (max-width: 959px) {
.rs-handle--left {
display: none;
}
}
@media (min-width: 1280px) {
.rs-handle--right {
display: block;
}
}

/* ================================================================
Content Page Enhancements
================================================================ */
Expand Down
2 changes: 2 additions & 0 deletions site/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import HomeHeroVisual from './components/HomeHeroVisual.vue'
import ProofStrip from './components/ProofStrip.vue'
import HomeRoadmap from './components/HomeRoadmap.vue'
import FontSizeSwitcher from './components/FontSizeSwitcher.vue'
import ResizableSidebar from './components/ResizableSidebar.vue'
import { setupMermaid } from './mermaid-client'
import './custom.css'

export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'layout-top': () => h(ResizableSidebar),
'home-hero-image': () => h(HomeHeroVisual),
'home-hero-actions-after': () => h('div', { class: 'proof-on-mobile' }, [h(ProofStrip)]),
'home-hero-after': () => h('div', { class: 'proof-on-desktop' }, [h(ProofStrip)]),
Expand Down
Loading