feat: 微信读书式阅读体验 — 主题/字体/字号/行距调节 + 阅读进度条

- Aa 阅读工具栏:5 种字体(系统/宋体/黑体/楷体/等宽)、字号 13-24px、三档行距、4 种主题(明亮/纸感/护眼绿/夜间),localStorage 持久化
- 正文阅读宽度收敛至 46rem(~736px),行高 1.85,符合长文阅读最佳实践
- 顶部阅读进度条 + 返回顶部按钮
- markdown.css 全量 CSS 变量化:表格/代码/引用/标题随主题联动,夜间模式自动适配
- 主题切换平滑过渡 + 面板弹出动画 + 页面淡入
- 移动端右下角 Aa FAB(与 TOC 按钮错开),面板全宽适配 390px
This commit is contained in:
OpenClaw Sub-agent
2026-08-03 14:37:06 +10:00
parent fa3265d848
commit e45f4a5ed5
2 changed files with 673 additions and 54 deletions
+551 -14
View File
@@ -27,6 +27,103 @@
--apple-radius-sm: 14px; --apple-radius-sm: 14px;
--apple-shadow-card: 0 4px 24px rgba(0, 0, 0, 0.05); --apple-shadow-card: 0 4px 24px rgba(0, 0, 0, 0.05);
--apple-shadow-lift: 0 18px 44px rgba(0, 0, 0, 0.10); --apple-shadow-lift: 0 18px 44px rgba(0, 0, 0, 0.10);
/* Reader surface (cards) — follows theme */
--reader-surface: #ffffff;
--reader-surface-2: #fafafa;
--reader-control: #f5f5f7;
--reader-hover: rgba(0, 0, 0, 0.05);
--reader-progress: linear-gradient(90deg, #0071e3 0%, #7c3aed 100%);
--reader-font-size: 17px;
--reader-line-height: 1.85;
}
/* ============================================================
Reader themes (微信读书-style): light / paper / green / night
Overrides the --apple-* + --md-* design tokens so the whole
page (cards, TOC, tables, code, quotes) re-skins together.
============================================================ */
body.reader-paper {
--apple-bg: #ece5d5;
--apple-text: #40392e;
--apple-gray: #8a8272;
--apple-separator: rgba(64, 57, 46, 0.10);
--reader-surface: #faf6ec;
--reader-surface-2: #f3ede0;
--reader-control: #efe8d8;
--reader-hover: rgba(64, 57, 46, 0.07);
--reader-progress: linear-gradient(90deg, #b8860b 0%, #8a6d1f 100%);
--md-text: #40392e;
--md-muted: #8a8272;
--md-heading: #332d23;
--md-border: #ddd4c0;
--md-accent: #9a6b1f;
--md-accent-soft: rgba(154, 107, 31, 0.12);
--md-table-head-bg: #efe8d8;
--md-table-zebra: rgba(154, 130, 60, 0.05);
--md-code-bg: #f0e9da;
--md-code-text: #4a4234;
--md-pre-bg: #3d362a;
--md-pre-text: #e8e0cd;
--md-quote-bg: rgba(154, 130, 60, 0.07);
--md-hr: #ddd4c0;
}
body.reader-green {
--apple-bg: #dde8da;
--apple-text: #26342a;
--apple-gray: #5f7163;
--apple-separator: rgba(38, 52, 42, 0.10);
--reader-surface: #eef5ec;
--reader-surface-2: #e3eee0;
--reader-control: #dde8da;
--reader-hover: rgba(38, 52, 42, 0.06);
--reader-progress: linear-gradient(90deg, #2e8b57 0%, #1e6b3c 100%);
--md-text: #26342a;
--md-muted: #5f7163;
--md-heading: #1d2a20;
--md-border: #c9dac4;
--md-accent: #1e7b34;
--md-accent-soft: rgba(30, 123, 52, 0.10);
--md-table-head-bg: #e3eee0;
--md-table-zebra: rgba(46, 139, 87, 0.05);
--md-code-bg: #e3eee0;
--md-code-text: #2c3a30;
--md-pre-bg: #22301f;
--md-pre-text: #dce8d9;
--md-quote-bg: rgba(46, 139, 87, 0.06);
--md-hr: #c9dac4;
}
body.reader-night {
--apple-bg: #131316;
--apple-text: #d4d4dc;
--apple-gray: #8b8b96;
--apple-blue: #4d9fff;
--apple-blue-hover: #66aeff;
--apple-green: #4caf7d;
--apple-purple: #c58af9;
--apple-red: #ff6b61;
--apple-separator: rgba(255, 255, 255, 0.10);
--reader-surface: #1c1c22;
--reader-surface-2: #232329;
--reader-control: #26262e;
--reader-hover: rgba(255, 255, 255, 0.07);
--reader-progress: linear-gradient(90deg, #4d9fff 0%, #c58af9 100%);
--apple-shadow-card: 0 4px 24px rgba(0, 0, 0, 0.35);
--apple-shadow-lift: 0 18px 44px rgba(0, 0, 0, 0.55);
--md-text: #c4c4cf;
--md-muted: #8b8b96;
--md-heading: #ececf2;
--md-border: #33333c;
--md-accent: #7db4ff;
--md-accent-soft: rgba(77, 159, 255, 0.14);
--md-table-head-bg: #26262e;
--md-table-zebra: rgba(255, 255, 255, 0.03);
--md-code-bg: #2a2a33;
--md-code-text: #e0e0e8;
--md-pre-bg: #0d0d10;
--md-pre-text: #c8c8d4;
--md-quote-bg: rgba(255, 255, 255, 0.04);
--md-hr: #2c2c34;
} }
body { body {
@@ -36,10 +133,11 @@
color: var(--apple-text); color: var(--apple-text);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
transition: background-color 0.4s var(--apple-ease), color 0.4s var(--apple-ease);
} }
.apple-card { .apple-card {
background: #fff; background: var(--reader-surface);
border-radius: var(--apple-radius-lg); border-radius: var(--apple-radius-lg);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: var(--apple-shadow-card); box-shadow: var(--apple-shadow-card);
@@ -108,7 +206,7 @@
display: flex; display: flex;
} }
.modal-content { .modal-content {
background: #fff; background: var(--reader-surface);
border-radius: var(--apple-radius-md); border-radius: var(--apple-radius-md);
width: 100%; width: 100%;
max-width: 26rem; max-width: 26rem;
@@ -404,11 +502,12 @@
white-space: nowrap; white-space: nowrap;
} }
.main-content { .main-content {
background: #fff; background: var(--reader-surface);
border-radius: var(--apple-radius-lg); border-radius: var(--apple-radius-lg);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: var(--apple-shadow-card); box-shadow: var(--apple-shadow-card);
padding: clamp(1.25rem, 4vw, 2.5rem); padding: clamp(1.25rem, 4vw, 2.5rem);
transition: background-color 0.4s var(--apple-ease), border-color 0.4s var(--apple-ease);
} }
/* ===== Table of Contents Layout ===== */ /* ===== Table of Contents Layout ===== */
@@ -422,7 +521,7 @@
.post-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; flex-wrap: wrap; max-width: 100%; } .post-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; flex-wrap: wrap; max-width: 100%; }
.post-meta { display: flex; flex-direction: column; gap: 0.85rem; margin-bottom: 1.25rem; } .post-meta { display: flex; flex-direction: column; gap: 0.85rem; margin-bottom: 1.25rem; }
.post-summary { position: relative; padding-left: 1rem; border-left: 3px solid rgba(0, 113, 227, 0.35); } .post-summary { position: relative; padding-left: 1rem; border-left: 3px solid rgba(0, 113, 227, 0.35); }
.post-summary p { font-size: 1.08rem; color: var(--apple-gray); font-style: italic; margin: 0; } .post-summary p { font-size: 1.02rem; color: var(--apple-gray); font-style: normal; margin: 0; line-height: 1.7; }
.post-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; } .post-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.post-timestamps { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; font-size: 0.85rem; color: var(--apple-gray); background: var(--apple-bg); border-radius: 980px; padding: 0.6rem 1.1rem; } .post-timestamps { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; font-size: 0.85rem; color: var(--apple-gray); background: var(--apple-bg); border-radius: 980px; padding: 0.6rem 1.1rem; }
.post-timestamps .timestamp-item { display: flex; align-items: center; } .post-timestamps .timestamp-item { display: flex; align-items: center; }
@@ -474,11 +573,12 @@
} }
} }
#toc-sidebar-inner > div { #toc-sidebar-inner > div {
background: #fff; background: var(--reader-surface);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid rgba(0, 0, 0, 0.04);
border-radius: var(--apple-radius-md); border-radius: var(--apple-radius-md);
box-shadow: var(--apple-shadow-card); box-shadow: var(--apple-shadow-card);
padding: 1.25rem; padding: 1.25rem;
transition: background-color 0.4s var(--apple-ease), border-color 0.4s var(--apple-ease);
} }
.toc-card-label { .toc-card-label {
font-size: 0.7rem; font-weight: 600; color: var(--apple-gray); font-size: 0.7rem; font-weight: 600; color: var(--apple-gray);
@@ -570,10 +670,257 @@
} }
#toc-nav-mobile .toc-link:hover { color: var(--apple-blue); background-color: rgba(0, 113, 227, 0.08); } #toc-nav-mobile .toc-link:hover { color: var(--apple-blue); background-color: rgba(0, 113, 227, 0.08); }
#toc-nav-mobile .toc-link.toc-active { color: var(--apple-blue); border-left-color: var(--apple-blue); font-weight: 500; } #toc-nav-mobile .toc-link.toc-active { color: var(--apple-blue); border-left-color: var(--apple-blue); font-weight: 500; }
/* ============================================================
Reader typography — 微信读书-style reading experience
============================================================ */
/* Comfortable measure: ~68-75 chars per line */
.post-article {
font-size: var(--reader-font-size, 17px);
line-height: var(--reader-line-height, 1.85);
max-width: 46rem;
margin-left: auto;
margin-right: auto;
transition: font-size 0.25s var(--apple-ease), line-height 0.25s var(--apple-ease);
}
.post-article * { font-family: var(--reader-font-family, inherit); }
.post-article code, .post-article pre, .post-article code * {
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Cascadia Code', 'JetBrains Mono', Consolas, monospace !important;
}
.post-article h1, .post-article h2, .post-article h3, .post-article h4 {
font-family: var(--reader-font-family, inherit);
scroll-margin-top: 5.5rem;
}
.post-article table { font-size: 0.92em; }
/* Font families */
.reader-font-system { --reader-font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Noto Sans SC", "Microsoft YaHei", "Segoe UI", sans-serif; }
.reader-font-serif { --reader-font-family: "Songti SC", "Noto Serif SC", "Source Han Serif SC", "STSong", "SimSun", Georgia, "Times New Roman", serif; }
.reader-font-hei { --reader-font-family: "PingFang SC", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei", "Heiti SC", "SimHei", sans-serif; }
.reader-font-kai { --reader-font-family: "Kaiti SC", "KaiTi", "STKaiti", "Noto Serif SC", "Songti SC", serif; }
.reader-font-mono { --reader-font-family: "SF Mono", "Menlo", "Monaco", "Cascadia Code", "JetBrains Mono", Consolas, monospace; }
/* ============================================================
Reading progress bar (top of viewport, below navbar)
============================================================ */
#reading-progress {
position: fixed;
top: 0; left: 0;
height: 3px;
width: 0%;
background: var(--reader-progress);
border-radius: 0 3px 3px 0;
z-index: 100;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
transition: width 0.12s linear;
pointer-events: none;
}
/* ============================================================
Aa reading toolbar
============================================================ */
.reader-toolbar-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 36px; height: 36px; border-radius: 50%;
border: none; cursor: pointer;
background: var(--reader-control);
color: var(--apple-text);
font-size: 0.95rem; font-weight: 700; letter-spacing: 0.02em;
transition: background 0.25s ease, transform 0.2s var(--apple-ease), box-shadow 0.25s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.reader-toolbar-btn:hover { background: var(--reader-hover); transform: translateY(-1px); }
.reader-toolbar-btn:active { transform: scale(0.94); }
.reader-toolbar-btn.active { background: var(--apple-blue); color: #fff; }
#reader-panel {
position: fixed;
right: 1.25rem;
bottom: 5.75rem;
z-index: 90;
width: min(21rem, calc(100vw - 2rem));
background: var(--reader-surface);
border: 1px solid rgba(0, 0, 0, 0.07);
border-radius: var(--apple-radius-md);
box-shadow: var(--apple-shadow-lift);
padding: 1.1rem 1.2rem 1.2rem;
opacity: 0;
transform: translateY(12px) scale(0.98);
pointer-events: none;
transition: opacity 0.28s var(--apple-ease), transform 0.28s var(--apple-ease);
}
#reader-panel.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.rp-section { margin-bottom: 1rem; }
.rp-section:last-child { margin-bottom: 0; }
.rp-label {
font-size: 0.68rem; font-weight: 700; color: var(--apple-gray);
text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 0.5rem;
}
/* Theme swatches */
.rp-themes { display: flex; gap: 0.75rem; }
.rp-swatch {
width: 34px; height: 34px; border-radius: 50%;
border: 2px solid transparent; cursor: pointer;
position: relative; transition: transform 0.2s var(--apple-ease), border-color 0.2s ease;
}
.rp-swatch:hover { transform: scale(1.1); }
.rp-swatch.active { border-color: var(--apple-blue); box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.18); }
.rp-swatch svg { position: absolute; inset: 0; margin: auto; width: 14px; height: 14px; color: #fff; opacity: 0; transition: opacity 0.2s; }
.rp-swatch.active svg { opacity: 1; }
.rp-swatch-light { background: #ffffff; border-color: #d1d1d6; }
.rp-swatch-paper { background: #faf6ec; border-color: #ddd4c0; }
.rp-swatch-green { background: #eef5ec; border-color: #c9dac4; }
.rp-swatch-night { background: #1c1c22; border-color: #44444e; }
/* Font chips */
.rp-fonts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.45rem; }
.rp-font-chip {
padding: 0.45rem 0.3rem; border-radius: var(--apple-radius-sm);
border: 1px solid rgba(0, 0, 0, 0.08); background: var(--reader-surface-2);
cursor: pointer; text-align: center; color: var(--apple-text);
transition: border-color 0.2s, background 0.2s;
}
.rp-font-chip:hover { background: var(--reader-hover); }
.rp-font-chip.active { border-color: var(--apple-blue); background: rgba(0, 113, 227, 0.08); }
.rp-font-chip .rp-font-name { display: block; font-size: 1rem; line-height: 1.3; }
.rp-font-chip .rp-font-meta { display: block; font-size: 0.62rem; color: var(--apple-gray); margin-top: 0.15rem; }
.rp-font-chip[data-font="system"] .rp-font-name { font-family: -apple-system, "PingFang SC", sans-serif; }
.rp-font-chip[data-font="serif"] .rp-font-name { font-family: "Songti SC", "Noto Serif SC", serif; }
.rp-font-chip[data-font="hei"] .rp-font-name { font-family: "PingFang SC", "Heiti SC", "Microsoft YaHei", sans-serif; }
.rp-font-chip[data-font="kai"] .rp-font-name { font-family: "Kaiti SC", "KaiTi", "STKaiti", serif; }
.rp-font-chip[data-font="mono"] .rp-font-name { font-family: "SF Mono", Menlo, Consolas, monospace; }
/* Size & spacing */
.rp-row { display: flex; align-items: center; gap: 0.6rem; }
.rp-size-btn {
width: 38px; height: 38px; border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.1); background: var(--reader-surface-2);
color: var(--apple-text); font-weight: 700; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center;
transition: background 0.2s, transform 0.15s var(--apple-ease);
}
.rp-size-btn:hover { background: var(--reader-hover); }
.rp-size-btn:active { transform: scale(0.92); }
.rp-size-btn[data-action="dec"] { font-size: 0.85rem; }
.rp-size-btn[data-action="inc"] { font-size: 1.15rem; }
.rp-size-val { flex: 1; text-align: center; font-size: 0.85rem; color: var(--apple-gray); font-variant-numeric: tabular-nums; }
.rp-spacing { display: flex; gap: 0.45rem; }
.rp-spacing-btn {
flex: 1; padding: 0.45rem 0; border-radius: var(--apple-radius-sm);
border: 1px solid rgba(0, 0, 0, 0.08); background: var(--reader-surface-2);
color: var(--apple-text); font-size: 0.75rem; cursor: pointer; text-align: center;
transition: border-color 0.2s, background 0.2s;
}
.rp-spacing-btn:hover { background: var(--reader-hover); }
.rp-spacing-btn.active { border-color: var(--apple-blue); background: rgba(0, 113, 227, 0.08); }
.rp-reset {
width: 100%; margin-top: 0.9rem; padding: 0.5rem;
border-radius: var(--apple-radius-sm); border: 1px dashed rgba(0, 0, 0, 0.15);
background: transparent; color: var(--apple-gray); font-size: 0.75rem; cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.rp-reset:hover { color: var(--apple-text); border-color: var(--apple-blue); }
/* Backdrop for closing panel on outside click (invisible) */
#reader-panel-backdrop {
position: fixed; inset: 0; z-index: 85; display: none;
}
#reader-panel.open ~ #reader-panel-backdrop { display: block; }
/* Aa button placement: inline in actions row (desktop) + floating (mobile) */
.reader-toolbar-wrap { display: inline-flex; align-items: center; }
#reader-fab {
display: none;
position: fixed;
bottom: 5.75rem; right: 1.5rem;
z-index: 41;
width: 3rem; height: 3rem;
border-radius: 9999px;
background: var(--apple-blue); color: #fff;
border: none; cursor: pointer;
font-size: 1.1rem; font-weight: 800;
box-shadow: 0 12px 28px rgba(0, 113, 227, 0.32);
align-items: center; justify-content: center;
transition: background-color 0.2s ease, transform 0.2s var(--apple-ease);
}
#reader-fab:hover { transform: translateY(-1px); }
@media (max-width: 1023px) {
#reader-fab { display: flex; }
#reader-panel {
bottom: 9.25rem;
right: 1.25rem;
}
}
@media (max-width: 640px) {
#reader-panel {
left: 1rem;
right: 1rem;
width: auto;
bottom: 9.4rem;
}
}
/* Scroll-to-top mini button */
#back-to-top {
display: none;
position: fixed;
bottom: 1.5rem; left: 1.5rem;
z-index: 41;
width: 2.75rem; height: 2.75rem;
border-radius: 9999px;
border: 1px solid rgba(0, 0, 0, 0.08);
background: var(--reader-surface);
color: var(--apple-gray);
box-shadow: var(--apple-shadow-card);
cursor: pointer;
align-items: center; justify-content: center;
transition: opacity 0.3s ease, transform 0.2s var(--apple-ease), color 0.2s;
}
#back-to-top:hover { color: var(--apple-blue); transform: translateY(-1px); }
#back-to-top.visible { display: flex; }
@media (min-width: 1024px) {
#back-to-top { display: none !important; }
}
/* Night theme tweaks for controls */
body.reader-night .rp-size-btn,
body.reader-night .rp-spacing-btn,
body.reader-night .rp-font-chip {
background: var(--reader-control);
}
body.reader-night .action-btn-secondary { background: var(--reader-control); }
body.reader-night .apple-btn-secondary { background: var(--reader-control); color: var(--apple-text); }
body.reader-night .post-timestamps { background: var(--reader-control); }
body.reader-night .post-summary { border-left-color: rgba(77, 159, 255, 0.4); }
body.reader-night .think-header { background: var(--reader-control); }
body.reader-night .think-header:hover { background: var(--reader-hover); }
body.reader-night .think-panel { background: var(--reader-surface); }
body.reader-night .append-textarea { background: var(--reader-control); color: var(--apple-text); }
body.reader-night .append-textarea:focus { background: var(--reader-surface-2); }
body.reader-night .audio-player { background: var(--reader-surface-2); }
body.reader-night .voice-controls { background: var(--reader-surface-2); }
body.reader-night .voice-select-compact,
body.reader-night .custom-voice-input { background: var(--reader-control); color: var(--apple-text); }
body.reader-night .toc-mobile-btn-back { background: var(--reader-control); }
body.reader-night #toc-sidebar .toc-h3,
body.reader-night #toc-sidebar .toc-h4,
body.reader-night #toc-nav-mobile .toc-h3,
body.reader-night #toc-nav-mobile .toc-h4 { color: #a5a5b0; }
body.reader-night .post-summary p { color: #a5a5b0; }
body.reader-night .prose blockquote { color: #a9a9b4; }
/* Reveal animation on load */
@keyframes readerFadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.main-content { animation: readerFadeIn 0.5s var(--apple-ease) both; }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<!-- Reading progress bar -->
<div id="reading-progress" aria-hidden="true"></div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div id="deleteModal" class="modal" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <div id="deleteModal" class="modal" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="modal-content"> <div class="modal-content">
@@ -624,6 +971,10 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="post-actions"> <div class="post-actions">
<div class="reader-toolbar-wrap">
<button id="reader-toolbar-btn" onclick="toggleReaderPanel()" class="reader-toolbar-btn"
title="{% trans 'Reading settings' %}" aria-label="{% trans 'Reading settings' %}">Aa</button>
</div>
<button onclick="toggleTTS()" class="action-btn action-btn-purple"> <button onclick="toggleTTS()" class="action-btn action-btn-purple">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -815,7 +1166,7 @@
</div> </div>
<!-- Content Section --> <!-- Content Section -->
<article class="prose max-w-none prose-lg post-article"> <article id="post-article" class="prose max-w-none prose-lg post-article">
{% markdown_with_tasks post.content post.task_states %} {% markdown_with_tasks post.content post.task_states %}
</article> </article>
@@ -841,6 +1192,79 @@
</svg> </svg>
</button> </button>
<!-- Mobile Aa floating button -->
<button id="reader-fab" onclick="toggleReaderPanel()" title="{% trans 'Reading settings' %}" aria-label="{% trans 'Reading settings' %}">Aa</button>
<!-- Reader settings panel -->
<div id="reader-panel" role="dialog" aria-label="{% trans 'Reading settings' %}">
<div class="rp-section">
<p class="rp-label">{% trans "Theme" %}</p>
<div class="rp-themes">
<button class="rp-swatch rp-swatch-light" data-theme="light" onclick="setReaderTheme('light')" title="{% trans 'Light' %}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
</button>
<button class="rp-swatch rp-swatch-paper" data-theme="paper" onclick="setReaderTheme('paper')" title="{% trans 'Paper' %}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
</button>
<button class="rp-swatch rp-swatch-green" data-theme="green" onclick="setReaderTheme('green')" title="{% trans 'Green' %}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
</button>
<button class="rp-swatch rp-swatch-night" data-theme="night" onclick="setReaderTheme('night')" title="{% trans 'Night' %}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
</button>
</div>
</div>
<div class="rp-section">
<p class="rp-label">{% trans "Font" %}</p>
<div class="rp-fonts">
<button class="rp-font-chip" data-font="system" onclick="setReaderFont('system')">
<span class="rp-font-name">系统</span><span class="rp-font-meta">System</span>
</button>
<button class="rp-font-chip" data-font="serif" onclick="setReaderFont('serif')">
<span class="rp-font-name">宋体</span><span class="rp-font-meta">Serif</span>
</button>
<button class="rp-font-chip" data-font="hei" onclick="setReaderFont('hei')">
<span class="rp-font-name">黑体</span><span class="rp-font-meta">Sans</span>
</button>
<button class="rp-font-chip" data-font="kai" onclick="setReaderFont('kai')">
<span class="rp-font-name">楷体</span><span class="rp-font-meta">Kai</span>
</button>
<button class="rp-font-chip" data-font="mono" onclick="setReaderFont('mono')">
<span class="rp-font-name">等宽</span><span class="rp-font-meta">Mono</span>
</button>
</div>
</div>
<div class="rp-section">
<p class="rp-label">{% trans "Size" %}</p>
<div class="rp-row">
<button class="rp-size-btn" data-action="dec" onclick="changeReaderSize(-1)" aria-label="{% trans 'Smaller' %}">A</button>
<span class="rp-size-val" id="rp-size-val">17px</span>
<button class="rp-size-btn" data-action="inc" onclick="changeReaderSize(1)" aria-label="{% trans 'Larger' %}">A+</button>
</div>
</div>
<div class="rp-section">
<p class="rp-label">{% trans "Spacing" %}</p>
<div class="rp-spacing">
<button class="rp-spacing-btn" data-spacing="1.6" onclick="setReaderSpacing(1.6)">{% trans "Compact" %}</button>
<button class="rp-spacing-btn" data-spacing="1.85" onclick="setReaderSpacing(1.85)">{% trans "Cozy" %}</button>
<button class="rp-spacing-btn" data-spacing="2.15" onclick="setReaderSpacing(2.15)">{% trans "Relaxed" %}</button>
</div>
</div>
<button class="rp-reset" onclick="resetReaderPrefs()">↺ {% trans "Reset to defaults" %}</button>
</div>
<div id="reader-panel-backdrop" onclick="closeReaderPanel()"></div>
<!-- Back to top -->
<button id="back-to-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})" aria-label="{% trans 'Back to top' %}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"/>
</svg>
</button>
<!-- Mobile TOC backdrop --> <!-- Mobile TOC backdrop -->
<div id="toc-mobile-backdrop" aria-hidden="true"></div> <div id="toc-mobile-backdrop" aria-hidden="true"></div>
@@ -1437,5 +1861,118 @@ function regenerateWithVoice() {
if (backdrop) backdrop.addEventListener('click', closeMobileDrawer); if (backdrop) backdrop.addEventListener('click', closeMobileDrawer);
}); });
})(); })();
// ============================================================
// Reader preferences — 微信读书-style reading experience
// ============================================================
(function() {
var STORAGE_KEY = 'links-reader-prefs-v1';
var DEFAULTS = { theme: 'light', font: 'system', size: 17, spacing: 1.85 };
var prefs = Object.assign({}, DEFAULTS);
try {
var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
Object.assign(prefs, saved);
} catch (e) { /* ignore corrupt prefs */ }
function clampSize(v) { return Math.min(24, Math.max(13, v)); }
function clampSpacing(v) { return Math.min(2.3, Math.max(1.4, v)); }
function save() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); } catch (e) {}
}
function apply() {
var body = document.body;
// Theme
['light', 'paper', 'green', 'night'].forEach(function(t) {
body.classList.toggle('reader-' + t, t === prefs.theme);
});
// Font
['system', 'serif', 'hei', 'kai', 'mono'].forEach(function(f) {
body.classList.toggle('reader-font-' + f, f === prefs.font);
});
// Size + spacing on the article
var article = document.getElementById('post-article');
if (article) {
article.style.setProperty('--reader-font-size', prefs.size + 'px');
article.style.setProperty('--reader-line-height', prefs.spacing);
}
// Panel UI state
document.querySelectorAll('.rp-swatch').forEach(function(el) {
el.classList.toggle('active', el.getAttribute('data-theme') === prefs.theme);
});
document.querySelectorAll('.rp-font-chip').forEach(function(el) {
el.classList.toggle('active', el.getAttribute('data-font') === prefs.font);
});
document.querySelectorAll('.rp-spacing-btn').forEach(function(el) {
el.classList.toggle('active', parseFloat(el.getAttribute('data-spacing')) === prefs.spacing);
});
var sizeVal = document.getElementById('rp-size-val');
if (sizeVal) sizeVal.textContent = prefs.size + 'px';
}
// Reading progress bar + back-to-top visibility
var progressBar = document.getElementById('reading-progress');
var backToTop = document.getElementById('back-to-top');
var scrollTicking = false;
function onScroll() {
if (scrollTicking) return;
scrollTicking = true;
requestAnimationFrame(function() {
scrollTicking = false;
var doc = document.documentElement;
var total = doc.scrollHeight - window.innerHeight;
var pct = total > 0 ? (window.scrollY / total) * 100 : 0;
if (progressBar) progressBar.style.width = pct + '%';
if (backToTop) backToTop.classList.toggle('visible', window.scrollY > 600);
});
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
// Expose panel controls globally (used by inline onclick)
window.toggleReaderPanel = function() {
var panel = document.getElementById('reader-panel');
if (!panel) return;
var open = panel.classList.toggle('open');
syncReaderTriggers(open);
};
window.closeReaderPanel = function() {
var panel = document.getElementById('reader-panel');
if (panel) panel.classList.remove('open');
syncReaderTriggers(false);
};
function syncReaderTriggers(open) {
['reader-toolbar-btn', 'reader-fab'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.classList.toggle('active', open);
});
}
window.setReaderTheme = function(t) {
prefs.theme = t; save(); apply();
};
window.setReaderFont = function(f) {
prefs.font = f; save(); apply();
};
window.changeReaderSize = function(delta) {
prefs.size = clampSize(prefs.size + delta); save(); apply();
};
window.setReaderSpacing = function(s) {
prefs.spacing = clampSpacing(s); save(); apply();
};
window.resetReaderPrefs = function() {
prefs = Object.assign({}, DEFAULTS); save(); apply();
};
// Close panel on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeReaderPanel();
});
document.addEventListener('DOMContentLoaded', function() {
apply();
});
})();
</script> </script>
{% endblock %} {% endblock %}
+122 -40
View File
@@ -1,103 +1,185 @@
/* ============================================================
Markdown prose styling — theme-variable driven.
Defaults keep existing light appearance; the post detail
reader overrides --md-* variables per theme (paper/green/night).
============================================================ */
:root {
--md-bg: #ffffff; /* content background */
--md-text: #374151; /* body text */
--md-muted: #6b7280; /* secondary text */
--md-heading: #111827; /* headings */
--md-border: #e5e7eb; /* table / block borders */
--md-accent: #2563eb; /* links */
--md-accent-soft: rgba(37, 99, 235, 0.08);
--md-table-head-bg: #f3f4f6;
--md-table-zebra: rgba(0, 0, 0, 0.02);
--md-code-bg: #f3f4f6;
--md-code-text: #111827;
--md-pre-bg: #1f2937;
--md-pre-text: #e5e7eb;
--md-quote-bg: rgba(0, 0, 0, 0.02);
--md-hr: #e5e7eb;
--md-radius: 12px;
}
.prose { .prose {
color: #374151; color: var(--md-text);
max-width: none; max-width: none;
line-height: 1.85;
} }
.prose p { .prose p {
margin-top: 1.25em; margin-top: 1.3em;
margin-bottom: 1.25em; margin-bottom: 1.3em;
overflow-wrap: break-word;
} }
.prose a { .prose a {
color: #2563eb; color: var(--md-accent);
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
.prose a:hover {
text-decoration-thickness: 2px;
} }
.prose strong { .prose strong {
font-weight: 600; font-weight: 700;
color: var(--md-heading);
} }
.prose ul { .prose ul {
margin-top: 1.25em; margin-top: 1.1em;
margin-bottom: 1.25em; margin-bottom: 1.1em;
list-style-type: disc; list-style-type: disc;
padding-left: 1.625em; padding-left: 1.625em;
} }
.prose ol { .prose ol {
margin-top: 1.25em; margin-top: 1.1em;
margin-bottom: 1.25em; margin-bottom: 1.1em;
list-style-type: decimal; list-style-type: decimal;
padding-left: 1.625em; padding-left: 1.625em;
} }
.prose li {
margin-top: 0.35em;
margin-bottom: 0.35em;
}
.prose li > ul,
.prose li > ol {
margin-top: 0.3em;
margin-bottom: 0.3em;
}
.prose h1 { .prose h1 {
font-size: 2.25em; font-size: 2.1em;
margin-top: 0; margin-top: 0;
margin-bottom: 0.8888889em; margin-bottom: 0.8em;
line-height: 1.1111111; line-height: 1.25;
font-weight: 600; font-weight: 700;
color: var(--md-heading);
letter-spacing: -0.015em;
} }
.prose h2 { .prose h2 {
font-size: 1.5em; font-size: 1.55em;
margin-top: 2em; margin-top: 2.2em;
margin-bottom: 1em; margin-bottom: 0.7em;
line-height: 1.3333333; line-height: 1.35;
font-weight: 600; font-weight: 700;
color: var(--md-heading);
letter-spacing: -0.01em;
} }
.prose h3 { .prose h3 {
font-size: 1.25em; font-size: 1.22em;
margin-top: 1.8em;
margin-bottom: 0.55em;
line-height: 1.5;
font-weight: 650;
color: var(--md-heading);
}
.prose h4 {
font-size: 1.05em;
margin-top: 1.6em; margin-top: 1.6em;
margin-bottom: 0.6em; margin-bottom: 0.5em;
line-height: 1.6; line-height: 1.5;
font-weight: 600; font-weight: 650;
color: var(--md-heading);
} }
.prose img { .prose img {
margin-top: 2em; margin-top: 2em;
margin-bottom: 2em; margin-bottom: 2em;
border-radius: var(--md-radius);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
} }
.prose code { .prose code {
color: #111827; color: var(--md-code-text);
font-weight: 600; font-weight: 600;
background-color: #f3f4f6; background-color: var(--md-code-bg);
padding: 0.2em 0.4em; padding: 0.2em 0.45em;
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.875em; font-size: 0.875em;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Cascadia Code', Consolas, monospace;
} }
.prose pre { .prose pre {
color: #e5e7eb; color: var(--md-pre-text);
background-color: #1f2937; background-color: var(--md-pre-bg);
overflow-x: auto; overflow-x: auto;
font-size: 0.875em; font-size: 0.875em;
line-height: 1.7142857; line-height: 1.7142857;
margin-top: 1.7142857em; margin-top: 1.7142857em;
margin-bottom: 1.7142857em; margin-bottom: 1.7142857em;
border-radius: 0.375rem; border-radius: var(--md-radius);
padding: 1em; padding: 1.1em 1.25em;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
} }
.prose pre code { .prose pre code {
background-color: transparent; background-color: transparent;
color: inherit; color: inherit;
padding: 0; padding: 0;
font-weight: 450;
} }
.prose blockquote { .prose blockquote {
font-style: italic; font-style: normal;
border-left: 4px solid #e5e7eb; border-left: 3px solid var(--md-border);
padding-left: 1em; background: var(--md-quote-bg);
margin: 1.25em 0; padding: 0.9em 1.2em;
margin: 1.4em 0;
border-radius: 0 var(--md-radius) var(--md-radius) 0;
color: var(--md-text);
}
.prose blockquote p {
margin: 0.4em 0;
}
.prose blockquote strong {
color: var(--md-heading);
}
.prose hr {
border: 0;
height: 1px;
background: var(--md-hr);
margin: 2.4em 0;
} }
.prose table { .prose table {
display: block; display: block;
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
border-collapse: collapse; border-collapse: collapse;
margin: 2em 0; margin: 1.8em 0;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
border-radius: var(--md-radius);
font-size: 0.93em;
} }
.prose table th { .prose table th {
background-color: #f3f4f6; background-color: var(--md-table-head-bg);
font-weight: 600; font-weight: 650;
text-align: left; text-align: left;
padding: 0.5em; padding: 0.6em 0.8em;
border: 1px solid #e5e7eb; border: 1px solid var(--md-border);
color: var(--md-heading);
white-space: nowrap;
} }
.prose table td { .prose table td {
padding: 0.5em; padding: 0.6em 0.8em;
border: 1px solid #e5e7eb; border: 1px solid var(--md-border);
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
vertical-align: top;
}
.prose table tr:nth-child(even) td {
background-color: var(--md-table-zebra);
} }