Improve the long-form reading experience

This commit is contained in:
hanxiankai
2026-09-01 15:57:25 +08:00
parent 4313ca23bb
commit 61804cdcee
5 changed files with 245 additions and 3 deletions
+76
View File
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute } from "vitepress";
const route = useRoute();
const progress = ref(0);
const active = ref(false);
let animationFrame: number | undefined;
let contentObserver: ResizeObserver | undefined;
function measureProgress() {
animationFrame = undefined;
const content = document.querySelector<HTMLElement>(".VPDoc .content-container");
if (!content) {
active.value = false;
progress.value = 0;
return;
}
const rootStyles = getComputedStyle(document.documentElement);
const navHeight = Number.parseFloat(rootStyles.getPropertyValue("--vp-nav-height")) || 64;
const contentTop = window.scrollY + content.getBoundingClientRect().top;
const start = Math.max(0, contentTop - navHeight);
const end = contentTop + content.scrollHeight - window.innerHeight;
const distance = end - start;
active.value = distance > 160;
progress.value = distance > 0 ? Math.min(1, Math.max(0, (window.scrollY - start) / distance)) : 1;
}
function scheduleMeasurement() {
if (animationFrame === undefined) animationFrame = window.requestAnimationFrame(measureProgress);
}
function observeContent() {
contentObserver?.disconnect();
const content = document.querySelector<HTMLElement>(".VPDoc .content-container");
if (content) contentObserver?.observe(content);
scheduleMeasurement();
}
onMounted(() => {
contentObserver = new ResizeObserver(scheduleMeasurement);
window.addEventListener("scroll", scheduleMeasurement, { passive: true });
window.addEventListener("resize", scheduleMeasurement);
observeContent();
});
watch(
() => route.path,
async () => {
progress.value = 0;
await nextTick();
observeContent();
},
);
onBeforeUnmount(() => {
window.removeEventListener("scroll", scheduleMeasurement);
window.removeEventListener("resize", scheduleMeasurement);
contentObserver?.disconnect();
if (animationFrame !== undefined) window.cancelAnimationFrame(animationFrame);
});
</script>
<template>
<div
v-show="active"
class="reading-progress"
aria-hidden="true"
data-reading-progress
:data-progress="(progress * 100).toFixed(1)"
>
<span class="reading-progress-value" :style="{ transform: `scaleX(${progress})` }" />
</div>
</template>
+6
View File
@@ -1,7 +1,13 @@
import DefaultTheme from "vitepress/theme";
import type { Theme } from "vitepress";
import { h } from "vue";
import ReadingProgress from "./ReadingProgress.vue";
import "./styles.css";
export default {
extends: DefaultTheme,
Layout: () =>
h(DefaultTheme.Layout, null, {
"layout-top": () => h(ReadingProgress),
}),
} satisfies Theme;
+124 -3
View File
@@ -6,10 +6,31 @@
--vp-c-tip-1: #176f5b;
--vp-c-tip-soft: rgba(23, 111, 91, 0.12);
--vp-layout-max-width: 1480px;
--reading-progress-color: #c0523a;
}
.VPDoc .content-container {
max-width: 820px;
max-width: 780px;
}
.reading-progress {
position: fixed;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: 2px;
overflow: hidden;
pointer-events: none;
}
.reading-progress-value {
display: block;
width: 100%;
height: 100%;
background: var(--reading-progress-color);
transform-origin: left center;
transition: transform 80ms linear;
}
.vp-doc h1,
@@ -19,8 +40,36 @@
}
.vp-doc h1 {
font-size: clamp(2rem, 5vw, 2.75rem);
line-height: 1.2;
max-width: 22ch;
font-size: 2.625rem;
line-height: 1.18;
text-wrap: balance;
}
.vp-doc h2,
.vp-doc h3 {
scroll-margin-top: calc(var(--vp-nav-height) + 20px);
text-wrap: balance;
}
.vp-doc p,
.vp-doc li {
line-height: 1.85;
}
.vp-doc p {
margin: 18px 0;
}
.vp-doc blockquote {
margin: 24px 0;
padding: 12px 18px;
border-left: 3px solid var(--vp-c-brand-1);
background: var(--vp-c-bg-soft);
}
.vp-doc blockquote > p {
margin: 0;
}
.vp-doc img {
@@ -116,6 +165,17 @@
white-space: nowrap;
}
.vp-doc h1 {
max-width: none;
font-size: 2rem;
line-height: 1.25;
}
.vp-doc p,
.vp-doc li {
line-height: 1.8;
}
.vp-doc .guide-paths {
grid-template-columns: 1fr;
}
@@ -129,3 +189,64 @@
overflow-x: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.reading-progress-value {
transition: none;
}
}
@media print {
@page {
margin: 18mm 16mm;
}
.VPNav,
.VPSidebar,
.VPDocAside,
.VPLocalNav,
.VPDocFooter,
.VPFooter,
.reading-progress {
display: none !important;
}
.VPContent,
.VPDoc,
.VPDoc .container,
.VPDoc .content,
.VPDoc .content-container {
width: auto !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
}
.vp-doc {
color: #111;
font-size: 11pt;
}
.vp-doc h1 {
max-width: none;
font-size: 26pt;
}
.vp-doc h1,
.vp-doc h2,
.vp-doc h3 {
break-after: avoid-page;
}
.vp-doc table,
.vp-doc pre,
.vp-doc blockquote,
.vp-doc img {
break-inside: avoid-page;
}
.vp-doc a {
color: inherit;
text-decoration: underline;
}
}