Files
wordpress-kratos-yang/assets/css/toc.css
T
laoyang 4a2d62b1aa 3.2.31 目录「正文内显示」与「浮动显示」拆成两个独立开关(#1)
原来「目录位置」是一个三选一下拉:正文中间 / 页面左侧 / 页面右侧,
等于把「放进正文」和「浮在页边」当成互斥的一件事 —— 想同时在正文
开头放一份、又在边上浮一份,做不到。

改成两个独立勾选,各自带子选项:

  · 在正文中显示 → 正文中的对齐(靠左 / 靠右)+ 自动插入位置
  · 浮动显示     → 浮动方向(页面左侧 / 页面右侧)

两个都勾就同时出现两份,只勾一个就只出现一种。四种组合都在后台
联动里逐个验证过(探针 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 字符串存在。
2026-10-25 13:37:26 +08:00

579 lines
14 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*!
* 文章目录(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;
}
}