原来「目录位置」是一个三选一下拉:正文中间 / 页面左侧 / 页面右侧,
等于把「放进正文」和「浮在页边」当成互斥的一件事 —— 想同时在正文
开头放一份、又在边上浮一份,做不到。
改成两个独立勾选,各自带子选项:
· 在正文中显示 → 正文中的对齐(靠左 / 靠右)+ 自动插入位置
· 浮动显示 → 浮动方向(页面左侧 / 页面右侧)
两个都勾就同时出现两份,只勾一个就只出现一种。四种组合都在后台
联动里逐个验证过(探针 70/0)。
新增能力「正文内靠左 / 靠右」是本单的主要新需求。实现上两个要点:
1. 必须给正文目录一个宽度上限(max-width: min(100%, 28em))。
块级盒子默认撑满正文栏,此时 margin:auto 没有剩余空间可分配,
「靠左/靠右」会是一个属性正确、画面纹丝不动的空操作。
2. 用 data-align 属性把方向带下去,CSS 只需两条规则,
不随方向数量膨胀。属性在 yang_toc_slot_html() 里输出。
语义收窄:手写 [TOC] 以前会让整份自动处理全停(连带 rail=false),
现在只手管「正文那一份」—— 手写位置优先,浮动仍由 float 开关独立
决定。这样「精确定位正文位置」和「全站边上都有目录」可以并存。
顺手修掉一处默认值错配:checkbox 用 !empty() 判断会把「键不存在」
和「键明确为空」混成一件事,导致新装站点后台框显示勾上、页面却
不出目录。改用 array_key_exists() 区分的三态判断。
老配置不迁移:3.2.30 的 toc_place 直接作废,不做兼容。
默认 inline=1 / float=0,升级后页面与升级前一致。
重构:把内容过滤器拆成 yang_toc_apply()(纯执行层)+ 运行时壳,
使设置组合能在单进程单测里逐个喂进去(PHP 无法重置函数 static)。
验证
逻辑 101/0 · 逻辑反向 15/15
旧回归 53/0
静态契约 58/0 · 反向 11/11
后台几何+交互 70/0 · 反向 11/11
侧栏几何 73/0 · 反向 8/8
正文对齐几何 23/0 · 反向 5/5(含阴性对照)
包内容契约 59/0 · 反向 9/9
正文对齐的几何探针量到真实的 Δleft = 348px(靠右比靠左),
而不是只断言 data-align 字符串存在。
579 lines
14 KiB
CSS
579 lines
14 KiB
CSS
/*!
|
||
* 文章目录(TOC)样式
|
||
* Yongquan Yang <yangyq.net>
|
||
*
|
||
* ── 设计原则 ────────────────────────────────────────────────
|
||
* 1. **不抢正文的视觉**:目录是辅助导航,不是内容。所以用浅灰背景、
|
||
* 细边框、小字号,让它在页面上"退后一步"。
|
||
* 2. **用主题主色**:强调色统一为 #00a2ff,与主题其他强调元素一致。
|
||
* 注意主题**没有**定义 CSS 变量(主色是各处硬编码的),所以这里
|
||
* 直接写字面值,不要臆造 var(--kratos-primary) 之类的变量 —— 那会
|
||
* 静默失效,目录永远是默认灰色,且看不出是哪里错了。
|
||
* 3. **移动端友好**:窄屏下缩进减小、字号略小,但不折叠成下拉 ——
|
||
* 目录本来就是给长文用的,手机上更需要它。
|
||
*/
|
||
|
||
.yang-toc {
|
||
margin: 24px 0;
|
||
border: 1px solid #ebeef5;
|
||
border-left: 3px solid #00a2ff;
|
||
border-radius: 3px;
|
||
background-color: #fafbfc;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* <details> 的默认三角标记去掉,用自己的图标 */
|
||
.yang-toc > summary {
|
||
list-style: none;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.yang-toc > summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
.yang-toc > summary::marker {
|
||
content: "";
|
||
}
|
||
|
||
.yang-toc-head {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 11px 16px;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
transition: color 0.15s ease;
|
||
}
|
||
|
||
.yang-toc-head:hover {
|
||
color: #00a2ff;
|
||
}
|
||
|
||
/* 折叠箭头:用 CSS 画的三角,不依赖图标字体或 SVG 文件 */
|
||
.yang-toc > summary .yang-toc-title::before {
|
||
content: "";
|
||
display: inline-block;
|
||
width: 0;
|
||
height: 0;
|
||
margin-right: 8px;
|
||
border-top: 5px solid transparent;
|
||
border-bottom: 5px solid transparent;
|
||
border-left: 6px solid currentColor;
|
||
vertical-align: middle;
|
||
transition: transform 0.18s ease;
|
||
}
|
||
|
||
/* 展开时箭头朝下(旋转 90°) */
|
||
.yang-toc[open] > summary .yang-toc-title::before {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
/* 不可折叠时(toggle=0)不显示箭头 */
|
||
.yang-toc:not(details) .yang-toc-head .yang-toc-title::before {
|
||
display: none;
|
||
}
|
||
|
||
.yang-toc-title {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
/* 条目数徽标 */
|
||
.yang-toc-count {
|
||
flex: 0 0 auto;
|
||
margin-left: 8px;
|
||
padding: 1px 7px;
|
||
border-radius: 9px;
|
||
background-color: #eef1f6;
|
||
color: #909399;
|
||
font-weight: 400;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.yang-toc-body {
|
||
padding: 0 16px 12px;
|
||
}
|
||
|
||
.yang-toc-list {
|
||
margin: 0;
|
||
padding-left: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
/* 二级以上的列表缩进,并加一条引导线 */
|
||
.yang-toc-list .yang-toc-list {
|
||
margin-left: 9px;
|
||
padding-left: 14px;
|
||
border-left: 1px dashed #e2e6ee;
|
||
}
|
||
|
||
.yang-toc-item {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
.yang-toc-link {
|
||
display: flex;
|
||
align-items: baseline;
|
||
padding: 4px 6px;
|
||
border-radius: 3px;
|
||
color: #4a4f57;
|
||
text-decoration: none;
|
||
transition: color 0.15s ease, background-color 0.15s ease;
|
||
}
|
||
|
||
.yang-toc-link:hover,
|
||
.yang-toc-link:focus {
|
||
background-color: #eef4ff;
|
||
color: #00a2ff;
|
||
text-decoration: none;
|
||
}
|
||
|
||
/* 序号 */
|
||
.yang-toc-num {
|
||
flex: 0 0 auto;
|
||
min-width: 1.9em;
|
||
margin-right: 5px;
|
||
color: #a8abb2;
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.yang-toc-text {
|
||
flex: 1 1 auto;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* 层级视觉差异:越深越淡、字号越小 */
|
||
.yang-toc-lv3 > .yang-toc-link {
|
||
color: #6b7280;
|
||
font-size: 13.5px;
|
||
}
|
||
|
||
.yang-toc-lv4 > .yang-toc-link,
|
||
.yang-toc-lv5 > .yang-toc-link,
|
||
.yang-toc-lv6 > .yang-toc-link {
|
||
color: #8b9099;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 当前阅读位置的条目高亮(JS 加 .is-active) */
|
||
.yang-toc-link.is-active {
|
||
background-color: #eef4ff;
|
||
color: #00a2ff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ── 标题锚点:点击跳转后被固定导航栏挡住的问题 ────────────────
|
||
* 主题的 .k-nav 是 fixed-top,滚动后固定约 67px 高。
|
||
* 纯 CSS 方案是给目标元素加 scroll-margin-top —— 浏览器原生支持,
|
||
* 且不依赖 JS(用 #anchor 直接访问、前进后退、外部链接进来都生效)。
|
||
*
|
||
* 为什么不用 JS 算偏移:JS 方案只在"点了目录"的情况下有效,
|
||
* 从外部带 #hash 进来、或按浏览器后退回到锚点时都不会触发。
|
||
* scroll-margin-top 是声明式的,所有情况自动生效。
|
||
*
|
||
* 67px 是导航栏高度;再加 16px 呼吸空间。管理工具栏(admin bar)
|
||
* 存在时再往上加,否则锚点会被工具栏盖住。
|
||
*/
|
||
.yang-toc-target,
|
||
.k-main .details .content h1[id],
|
||
.k-main .details .content h2[id],
|
||
.k-main .details .content h3[id],
|
||
.k-main .details .content h4[id],
|
||
.k-main .details .content h5[id],
|
||
.k-main .details .content h6[id] {
|
||
scroll-margin-top: 84px;
|
||
}
|
||
|
||
body.admin-bar .k-main .details .content h1[id],
|
||
body.admin-bar .k-main .details .content h2[id],
|
||
body.admin-bar .k-main .details .content h3[id],
|
||
body.admin-bar .k-main .details .content h4[id],
|
||
body.admin-bar .k-main .details .content h5[id],
|
||
body.admin-bar .k-main .details .content h6[id] {
|
||
scroll-margin-top: 116px;
|
||
}
|
||
|
||
/* 滚动到锚点时轻微闪一下,让用户确认"跳到这里了" */
|
||
@keyframes yang-toc-flash {
|
||
0%,
|
||
100% {
|
||
background-color: transparent;
|
||
}
|
||
30% {
|
||
background-color: #fffbe6;
|
||
}
|
||
}
|
||
|
||
.yang-toc-flash {
|
||
animation: yang-toc-flash 1.1s ease;
|
||
}
|
||
|
||
/* ── 窄屏 ── */
|
||
@media screen and (max-width: 768px) {
|
||
.yang-toc {
|
||
margin: 18px 0;
|
||
font-size: 13.5px;
|
||
}
|
||
|
||
.yang-toc-head {
|
||
padding: 10px 13px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.yang-toc-body {
|
||
padding: 0 10px 10px;
|
||
}
|
||
|
||
.yang-toc-list .yang-toc-list {
|
||
margin-left: 4px;
|
||
padding-left: 10px;
|
||
}
|
||
|
||
.yang-toc-link {
|
||
padding: 5px 5px;
|
||
}
|
||
|
||
.yang-toc-lv3 > .yang-toc-link {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
/* ── 正文内嵌槽(自动插入时用) ──────────────────────────────
|
||
* 自动插入的目录外面包一层 slot。加这层不是为了好看,是为了让
|
||
* 「目录在哪」这件事在 DOM 里有痕迹:插入位置变了、或者根本没插进去,
|
||
* 看 slot 的 class / 位置就能判断,不用去猜正文里的 <details> 是谁输出的。
|
||
*
|
||
* 边距放在 slot 上、把 .yang-toc 自身的 margin 归零:
|
||
* 两层都带 margin 时外边距会叠加,插在开头/结尾的那几种位置尤其难看
|
||
* (开头会顶出一块空白)。
|
||
*/
|
||
.yang-toc-slot {
|
||
margin: 24px 0;
|
||
}
|
||
|
||
.yang-toc-slot > .yang-toc {
|
||
margin: 0;
|
||
}
|
||
|
||
/* ── 正文内目录的左右对齐 ─────────────────────────────────────
|
||
* 需求里「靠左 / 靠右」指的是:目录块在**正文容器内部**贴左还是贴右,
|
||
* 不是浮到页面边上(那是 .yang-toc-rail 的职责)。
|
||
*
|
||
* 为什么必须收窄宽度:块级盒子默认撑满正文栏,此时靠左靠右在视觉上
|
||
* 完全一样 —— 只写 margin 的「对齐」是个**看起来实现了、实际是空操作**
|
||
* 的功能,量几何都量不出差别。所以这里给一个上限宽度,让对齐有意义。
|
||
*
|
||
* 用 min() 而不是固定 px:窄屏时自动退化成 100%,不会出现「手机上目录
|
||
* 只占一半、右边空一大块」。em 单位跟着字号走,用户改了基础字号也不会怪。
|
||
*
|
||
* 用 margin-left/right:auto 而不是 float:float 会让正文文字绕排,那是
|
||
* 另一回事(也会把 <details> 的折叠交互搞乱)。这里只做「在容器里摆哪边」。
|
||
*/
|
||
.yang-toc-slot[data-align] > .yang-toc {
|
||
max-width: min(100%, 28em);
|
||
}
|
||
|
||
.yang-toc-slot[data-align="left"] > .yang-toc {
|
||
margin-left: 0;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.yang-toc-slot[data-align="right"] > .yang-toc {
|
||
margin-left: auto;
|
||
margin-right: 0;
|
||
}
|
||
|
||
/* ── 侧边浮动目录 ─────────────────────────────────────────────
|
||
* 结构:贴边的一条细标签 + 一块可滑出的面板。
|
||
*
|
||
* 关键几何约束(容易被后来的改动破坏):
|
||
* · 容器是 0 宽的定位锚点,不可见、不吃点击(pointer-events:none);
|
||
* 真正可点的只有标签和面板。
|
||
* · 收起态面板必须**完全**移出视口(translateX(±100%)),
|
||
* 不能只做透明度。留一点"露头"看着舒服,但它会盖住正文列边缘,
|
||
* 窄屏上正好压掉正文的第一个字 —— 那种问题肉眼很难定位。
|
||
* · z-index 低于导航(Bootstrap navbar 是 1030),
|
||
* 万一两者在高度上相遇,导航仍然压在上面。
|
||
*/
|
||
.yang-toc-rail {
|
||
position: fixed;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 0;
|
||
z-index: 1000;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.yang-toc-rail-left {
|
||
left: 0;
|
||
}
|
||
|
||
.yang-toc-rail-right {
|
||
right: 0;
|
||
}
|
||
|
||
.yang-toc-rail > * {
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* 贴边标签:竖排文字,主色底 */
|
||
.yang-toc-rail-tab {
|
||
position: absolute;
|
||
top: 50%;
|
||
width: 34px;
|
||
padding: 14px 0;
|
||
border: 0;
|
||
border-radius: 0 6px 6px 0;
|
||
background-color: #00a2ff;
|
||
color: #fff;
|
||
box-shadow: 1px 0 6px rgba(0, 0, 0, 0.12);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
transform: translateY(-50%);
|
||
transition: opacity 0.18s ease, background-color 0.18s ease;
|
||
}
|
||
|
||
.yang-toc-rail-left .yang-toc-rail-tab {
|
||
left: 0;
|
||
}
|
||
|
||
.yang-toc-rail-right .yang-toc-rail-tab {
|
||
right: 0;
|
||
border-radius: 6px 0 0 6px;
|
||
box-shadow: -1px 0 6px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.yang-toc-rail-tab:hover,
|
||
.yang-toc-rail-tab:focus {
|
||
background-color: #0089d8;
|
||
}
|
||
|
||
.yang-toc-rail-tab-text {
|
||
display: block;
|
||
letter-spacing: 3px;
|
||
writing-mode: vertical-rl;
|
||
text-orientation: upright;
|
||
}
|
||
|
||
/* 展开时标签让位给面板(面板已经贴边盖住它了,留着只是碍眼) */
|
||
.yang-toc-rail.is-open .yang-toc-rail-tab {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 面板 */
|
||
.yang-toc-rail-panel {
|
||
position: absolute;
|
||
top: 50%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 288px;
|
||
max-height: 72vh;
|
||
border: 1px solid #ebeef5;
|
||
background-color: #fff;
|
||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14);
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.2s ease, transform 0.28s ease, visibility 0s linear 0.28s;
|
||
}
|
||
|
||
.yang-toc-rail-left .yang-toc-rail-panel {
|
||
left: 0;
|
||
border-left: 3px solid #00a2ff;
|
||
border-radius: 0 4px 4px 0;
|
||
transform: translate(-100%, -50%);
|
||
}
|
||
|
||
.yang-toc-rail-right .yang-toc-rail-panel {
|
||
right: 0;
|
||
border-right: 3px solid #00a2ff;
|
||
border-radius: 4px 0 0 4px;
|
||
transform: translate(100%, -50%);
|
||
}
|
||
|
||
.yang-toc-rail.is-open .yang-toc-rail-panel {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
transform: translate(0, -50%);
|
||
transition: opacity 0.2s ease, transform 0.28s ease, visibility 0s;
|
||
}
|
||
|
||
.yang-toc-rail-head {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
padding: 12px 12px 8px 14px;
|
||
border-bottom: 1px solid #f3f5f8;
|
||
}
|
||
|
||
.yang-toc-rail-title {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.yang-toc-rail-close {
|
||
flex: 0 0 auto;
|
||
width: 24px;
|
||
height: 24px;
|
||
margin-left: auto;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 3px;
|
||
background: none;
|
||
color: #909399;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.yang-toc-rail-close:hover,
|
||
.yang-toc-rail-close:focus {
|
||
background-color: #f2f4f8;
|
||
color: #00a2ff;
|
||
}
|
||
|
||
.yang-toc-rail-body {
|
||
flex: 1 1 auto;
|
||
overflow-y: auto;
|
||
padding: 10px 12px 12px;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
/* ── 窄屏:面板按视口宽度收,标签再窄一点 ── */
|
||
@media screen and (max-width: 768px) {
|
||
.yang-toc-rail-panel {
|
||
width: min(78vw, 300px);
|
||
max-height: 70vh;
|
||
}
|
||
|
||
.yang-toc-rail-tab {
|
||
width: 30px;
|
||
padding: 12px 0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.yang-toc-rail-head {
|
||
padding: 10px 10px 8px 12px;
|
||
}
|
||
|
||
.yang-toc-rail-body {
|
||
padding: 8px 10px 10px;
|
||
}
|
||
}
|
||
|
||
/* 动效敏感的用户:不要滑出动画,直接显隐 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.yang-toc-rail-panel,
|
||
.yang-toc-rail.is-open .yang-toc-rail-panel,
|
||
.yang-toc-rail-tab {
|
||
transition: none;
|
||
}
|
||
}
|
||
|
||
/* ── 打印:目录展开、去背景、不折行 ────────────────────────── */
|
||
@media print {
|
||
.yang-toc {
|
||
border: 1px solid #ccc;
|
||
background: none;
|
||
page-break-inside: avoid;
|
||
}
|
||
|
||
.yang-toc-count {
|
||
display: none;
|
||
}
|
||
|
||
.yang-toc-link {
|
||
color: #000;
|
||
}
|
||
|
||
/* 侧栏是屏幕上的快捷入口,纸上没有意义,还会盖住正文 */
|
||
.yang-toc-rail {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* ── 暗色主题适配 ──────────────────────────────────────────
|
||
* 主题没有内置暗色模式,但用户可能在「自定义 CSS」里加
|
||
* prefers-color-scheme。这里跟随同一个媒体查询,避免目录
|
||
* 在暗色下变成一块刺眼的白板。
|
||
*/
|
||
@media (prefers-color-scheme: dark) {
|
||
html.yang-dark .yang-toc {
|
||
border-color: #303640;
|
||
border-left-color: #00a2ff;
|
||
background-color: #1d2026;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-head {
|
||
color: #d5d9e0;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-count {
|
||
background-color: #2a2f38;
|
||
color: #8b929d;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-link {
|
||
color: #b6bcc6;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-link:hover,
|
||
html.yang-dark .yang-toc-link.is-active {
|
||
background-color: #252b34;
|
||
color: #00a2ff;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-list .yang-toc-list {
|
||
border-left-color: #333944;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-num {
|
||
color: #767d88;
|
||
}
|
||
|
||
/* 侧边目录:面板底色/边框/文字跟内嵌目录同一套暗色值,
|
||
否则暗色站点上会凭空出现一块白板。标签本身是主色底白字,
|
||
两种模式下都不用改。 */
|
||
html.yang-dark .yang-toc-rail-panel {
|
||
border-color: #303640;
|
||
background-color: #1d2026;
|
||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
html.yang-dark .yang-toc-rail-left .yang-toc-rail-panel {
|
||
border-left-color: #00a2ff;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-rail-right .yang-toc-rail-panel {
|
||
border-right-color: #00a2ff;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-rail-head {
|
||
border-bottom-color: #2a2f38;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-rail-title {
|
||
color: #d5d9e0;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-rail-close {
|
||
color: #8b929d;
|
||
}
|
||
|
||
html.yang-dark .yang-toc-rail-close:hover,
|
||
html.yang-dark .yang-toc-rail-close:focus {
|
||
background-color: #252b34;
|
||
color: #00a2ff;
|
||
}
|
||
} |