author: Chenzhu-Xie name: Library/xczphysics/STYLE/Theme/HHH tags: meta/library files: - HHH.js
note Note JS file now should be automatically downloaded and loaded. So, no need to carry out below 4 steps.
JS inject from CONFIG/View/Tree/Floatdanger Danger when remove this plug, better first: ${widgets.commandButton("Delete: HHH.js")}
local jsCode = [[
// Library/xczphysics/STYLE/Theme/HHH.js
// HHH v13 - 修复缩进 + 删除小方格
const STATE_KEY = "__xhHighlightState_v13";
// ==========================================
// 辅助函数
// ==========================================
/**
* 居中光标的辅助函数
* 尝试多种方式实现 "Navigate: Center Cursor" 效果
*/
async function centerCursor() {
// 给导航一点时间完成
await new Promise(resolve => setTimeout(resolve, 50));
try {
// 方法1: 使用 silverbullet.syscall (如果在正确的上下文中)
if (globalThis.silverbullet && typeof globalThis.silverbullet.syscall === 'function') {
await globalThis.silverbullet.syscall("editor.invokeCommand", "Navigate: Center Cursor");
return true;
}
} catch (e) {
// console.warn("[LinkFloater] syscall method failed:", e);
}
try {
// 方法2: 直接使用 editorView 滚动到光标位置
if (window.client && client.editorView) {
const view = client.editorView;
const cursorPos = view.state.selection.main.head;
// 获取光标的屏幕坐标
const coords = view.coordsAtPos(cursorPos);
if (coords) {
const viewRect = view.dom.getBoundingClientRect();
const viewHeight = viewRect.height;
// 计算目标滚动位置,使光标位于视图中心
const currentScrollTop = view.scrollDOM.scrollTop;
const cursorRelativeY = coords.top - viewRect.top + currentScrollTop;
const targetScrollTop = cursorRelativeY - viewHeight / 2;
view.scrollDOM.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'instant'
});
}
return true;
}
} catch (e) {
// console.warn("[LinkFloater] Direct scroll failed:", e);
}
return false;
}
/**
* 封装的导航函数 - 导航后自动居中光标
* @param {Object} options - client.navigate 的参数
*/
function navigateAndCenter(options) {
if (!window.client) return;
client.navigate(options);
// 异步执行居中,不阻塞导航
setTimeout(() => centerCursor(), 50);
}
// ==========================================
// 1. Model: 数据模型
// ==========================================
const DataModel = {
headings: [],
lastText: null,
getFullText() {
try {
if (window.client && client.editorView && client.editorView.state) {
return client.editorView.state.sliceDoc();
}
} catch (e) { console.warn(e); }
return "";
},
rebuildSync() {
const text = this.getFullText();
if (text === this.lastText && this.headings.length > 0) return;
this.lastText = text;
this.headings = [];
if (!text) return;
const codeBlockRanges = [];
const codeBlockRegex = /```[\s\S]*?```/gm;
let blockMatch;
while ((blockMatch = codeBlockRegex.exec(text)) !== null) {
codeBlockRanges.push({
start: blockMatch.index,
end: blockMatch.index + blockMatch[0].length
});
}
const regex = /^(#{1,6})\s+([^\n]*)$/gm;
let match;
while ((match = regex.exec(text)) !== null) {
const matchIndex = match.index;
const isInsideCodeBlock = codeBlockRanges.some(range =>
matchIndex >= range.start && matchIndex < range.end
);
if (isInsideCodeBlock) continue;
this.headings.push({
index: this.headings.length,
level: match[1].length,
text: match[2].trim(),
start: matchIndex,
end: matchIndex + match[0].length
});
}
},
findHeadingIndexByPos(pos) {
this.rebuildSync();
let bestIndex = -1;
for (let i = 0; i < this.headings.length; i++) {
if (this.headings[i].start <= pos) {
bestIndex = i;
} else {
break;
}
}
return bestIndex;
},
getFamilyIndices(targetIndex) {
const indices = new Set();
if (targetIndex < 0 || targetIndex >= this.headings.length) return indices;
const target = this.headings[targetIndex];
indices.add(targetIndex);
let currentLevel = target.level;
for (let i = targetIndex - 1; i >= 0; i--) {
const h = this.headings[i];
if (h.level < currentLevel) {
indices.add(i);
currentLevel = h.level;
if (currentLevel === 1) break;
}
}
for (let i = targetIndex + 1; i < this.headings.length; i++) {
const h = this.headings[i];
if (h.level <= target.level) break;
indices.add(i);
}
return indices;
},
getAncestors(targetIndex) {
if (targetIndex < 0) return [];
const target = this.headings[targetIndex];
const list = [target];
let currentLevel = target.level;
for (let i = targetIndex - 1; i >= 0; i--) {
const h = this.headings[i];
if (h.level < currentLevel) {
list.unshift(h);
currentLevel = h.level;
}
}
return list;
},
getDescendants(targetIndex) {
if (targetIndex < 0) return [];
const target = this.headings[targetIndex];
const list = [];
for (let i = targetIndex + 1; i < this.headings.length; i++) {
const h = this.headings[i];
if (h.level <= target.level) break;
list.push(h);
}
return list;
}
};
// ==========================================
// 2. View: 视图渲染
// ==========================================
const View = {
topContainerId: "sb-frozen-container-top",
bottomContainerId: "sb-frozen-container-bottom",
getContainer(id) {
let el = document.getElementById(id);
if (!el) {
el = document.createElement("div");
el.id = id;
el.className = "sb-frozen-container";
el.style.position = "fixed";
el.style.zIndex = "9999";
el.style.display = "none";
el.style.pointerEvents = "auto";
document.body.appendChild(el);
}
return el;
},
splitIntoColumns(items, itemHeight = 26) {
const maxHeight = window.innerHeight * 0.45;
const maxItemsPerCol = Math.max(3, Math.floor(maxHeight / itemHeight));
const columns = [];
for (let i = 0; i < items.length; i += maxItemsPerCol) {
columns.push(items.slice(i, i + maxItemsPerCol));
}
return columns;
},
/**
* 生成树状结构前缀
* @param {number} level - 当前标题层级
* @param {number} baseLevel - 基础层级
* @param {boolean} isLast - 是否是该层级最后一个
* @param {Array} parentIsLast - 父级是否为最后一个的数组
*/
generateTreePrefix(level, baseLevel, isLast, parentIsLast = []) {
if (level <= baseLevel) return "";
let prefix = "";
const depth = level - baseLevel;
// 使用不间断空格确保宽度一致
const SPACE = "\u00A0\u00A0"; // 两个不间断空格
for (let i = 0; i < depth - 1; i++) {
if (parentIsLast[i]) {
prefix += "\u00A0" + SPACE; // 父级是最后一个,用空白
} else {
prefix += "│" + SPACE; // 父级不是最后一个,用竖线
}
}
// 最后一个连接符
prefix += isLast ? "└─" : "├─";
return prefix;
},
/**
* 创建可悬浮展开的标题项
*/
createHeadingItem(h, baseLevel, isLast, parentIsLast, index, total) {
const wrapper = document.createElement("div");
wrapper.className = "sb-frozen-item-wrapper";
wrapper.style.display = "flex";
wrapper.style.alignItems = "center";
wrapper.style.gap = "2px";
// 树状前缀
const treePrefix = this.generateTreePrefix(h.level, baseLevel, isLast, parentIsLast);
if (treePrefix) {
const prefixSpan = document.createElement("span");
prefixSpan.className = "sb-frozen-tree-prefix";
prefixSpan.textContent = treePrefix;
prefixSpan.style.fontFamily = "monospace";
prefixSpan.style.fontSize = "10px";
prefixSpan.style.opacity = "0.5";
prefixSpan.style.whiteSpace = "pre"; // 保持空格
wrapper.appendChild(prefixSpan);
}
// 标题按钮
const div = document.createElement("div");
div.className = `sb-frozen-item sb-frozen-l${h.level}`;
const maxLen = 20;
const shortText = h.text.length > maxLen ? h.text.substring(0, maxLen) + "…" : h.text;
const fullText = h.text;
div.textContent = shortText;
div.title = fullText;
div.dataset.fullText = fullText;
div.dataset.shortText = shortText;
div.style.cursor = "pointer";
div.style.position = "relative";
// 层级指示器(左下角小字)
const levelIndicator = document.createElement("span");
levelIndicator.className = "sb-frozen-level-indicator";
levelIndicator.textContent = `H${h.level}`;
div.appendChild(levelIndicator);
// 悬浮展开
div.addEventListener("mouseenter", () => {
if (fullText !== shortText) {
// 保留层级指示器
div.childNodes[0].textContent = fullText;
div.classList.add("sb-frozen-expanded");
}
});
div.addEventListener("mouseleave", () => {
div.childNodes[0].textContent = shortText;
div.classList.remove("sb-frozen-expanded");
});
// 点击导航
div.onclick = (e) => {
e.stopPropagation();
if (window.client) {
const pagePath = client.currentPath();
navigateAndCenter({
path: pagePath,
details: { type: "header", header: h.text }
});
}
};
wrapper.appendChild(div);
return wrapper;
},
/**
* 计算 parentIsLast 数组
*/
computeParentIsLast(items, index, baseLevel) {
const currentLevel = items[index].level;
const result = [];
for (let lvl = baseLevel + 1; lvl < currentLevel; lvl++) {
// 检查在这个层级,当前项之后是否还有同级或更高级的项
let isLastAtThisLevel = true;
for (let j = index + 1; j < items.length; j++) {
if (items[j].level <= lvl) {
isLastAtThisLevel = false;
break;
}
}
result.push(isLastAtThisLevel);
}
return result;
},
/**
* 检查是否是同级中的最后一个
*/
isLastSibling(items, index) {
const currentLevel = items[index].level;
for (let j = index + 1; j < items.length; j++) {
if (items[j].level === currentLevel) return false;
if (items[j].level < currentLevel) return true;
}
return true;
},
renderTopBar(targetIndex, container) {
const el = this.getContainer(this.topContainerId);
if (targetIndex === -1) {
el.style.display = "none";
return;
}
const list = DataModel.getAncestors(targetIndex);
if (list.length === 0) {
el.style.display = "none";
return;
}
if (container) {
const rect = container.getBoundingClientRect();
el.style.left = (rect.left + 45) + "px";
el.style.top = (rect.top + 30) + "px";
}
el.innerHTML = "";
el.style.display = "flex";
el.style.flexDirection = "row";
el.style.gap = "8px";
el.style.alignItems = "flex-start";
const columns = this.splitIntoColumns(list);
const baseLevel = 0; // ancestors 从 1 级开始
columns.forEach((columnItems, colIndex) => {
const col = document.createElement("div");
col.className = "sb-frozen-col";
col.style.display = "flex";
col.style.flexDirection = "column";
col.style.alignItems = "flex-start";
col.style.gap = "2px";
if (colIndex === 0) {
const label = document.createElement("div");
label.textContent = "Context:";
label.style.fontSize = "10px";
label.style.opacity = "0.5";
label.style.marginBottom = "2px";
label.style.pointerEvents = "none";
col.appendChild(label);
} else {
const spacer = document.createElement("div");
spacer.textContent = "·";
spacer.style.fontSize = "10px";
spacer.style.opacity = "0.3";
spacer.style.marginBottom = "2px";
col.appendChild(spacer);
}
columnItems.forEach((h, idx) => {
const globalIdx = colIndex * Math.ceil(list.length / columns.length) + idx;
const isLast = this.isLastSibling(list, globalIdx);
const parentIsLast = this.computeParentIsLast(list, globalIdx, baseLevel);
col.appendChild(this.createHeadingItem(h, baseLevel, isLast, parentIsLast, idx, columnItems.length));
});
el.appendChild(col);
});
},
renderBottomBar(targetIndex, container) {
const el = this.getContainer(this.bottomContainerId);
if (targetIndex === -1) {
el.style.display = "none";
return;
}
const list = DataModel.getDescendants(targetIndex);
if (list.length === 0) {
el.style.display = "none";
return;
}
if (container) {
const rect = container.getBoundingClientRect();
el.style.left = (rect.left + 45) + "px";
el.style.bottom = "30px";
el.style.top = "auto";
}
el.innerHTML = "";
el.style.display = "flex";
el.style.flexDirection = "row";
el.style.gap = "8px";
el.style.alignItems = "flex-end";
const baseLevel = DataModel.headings[targetIndex]?.level || 1;
const columns = this.splitIntoColumns(list);
columns.forEach((columnItems, colIndex) => {
const col = document.createElement("div");
col.className = "sb-frozen-col";
col.style.display = "flex";
col.style.flexDirection = "column";
col.style.alignItems = "flex-start";
col.style.gap = "2px";
if (colIndex === 0) {
const label = document.createElement("div");
label.textContent = "Sub-sections:";
label.style.fontSize = "10px";
label.style.opacity = "0.5";
label.style.marginBottom = "2px";
label.style.pointerEvents = "none";
col.appendChild(label);
} else {
const spacer = document.createElement("div");
spacer.textContent = "·";
spacer.style.fontSize = "10px";
spacer.style.opacity = "0.3";
spacer.style.marginBottom = "2px";
col.appendChild(spacer);
}
columnItems.forEach((h, idx) => {
const globalIdx = colIndex * Math.ceil(list.length / columns.length) + idx;
const isLast = this.isLastSibling(list, globalIdx);
const parentIsLast = this.computeParentIsLast(list, globalIdx, baseLevel);
col.appendChild(this.createHeadingItem(h, baseLevel, isLast, parentIsLast, idx, columnItems.length));
});
el.appendChild(col);
});
},
applyHighlights(container, activeIndices) {
const cls = ["sb-active", "sb-active-anc", "sb-active-desc", "sb-active-current"];
container.querySelectorAll("." + cls.join(", .")).forEach(el => el.classList.remove(...cls));
if (!activeIndices || activeIndices.size === 0) return;
if (!window.client || !client.editorView) return;
const view = client.editorView;
const visibleHeadings = container.querySelectorAll(".sb-line-h1, .sb-line-h2, .sb-line-h3, .sb-line-h4, .sb-line-h5, .sb-line-h6");
visibleHeadings.forEach(el => {
try {
const pos = view.posAtDOM(el);
const idx = DataModel.findHeadingIndexByPos(pos + 1);
if (idx !== -1 && activeIndices.has(idx)) {
const h = DataModel.headings[idx];
if (pos >= h.start - 50 && pos <= h.end + 50) {
el.classList.add("sb-active");
if (idx === window[STATE_KEY].currentIndex) {
el.classList.add("sb-active-current");
} else {
const mainIdx = window[STATE_KEY].currentIndex;
const currentLevel = DataModel.headings[mainIdx].level;
if (idx < mainIdx && DataModel.headings[idx].level < currentLevel) {
el.classList.add("sb-active-anc");
} else {
el.classList.add("sb-active-desc");
}
}
}
}
} catch (e) {}
});
}
};
// ==========================================
// 3. Controller
// ==========================================
export function enableHighlight(opts = {}) {
const containerSelector = opts.containerSelector || "#sb-main";
const bind = () => {
const container = document.querySelector(containerSelector);
if (!container || !window.client || !client.editorView) {
requestAnimationFrame(bind);
return;
}
if (window[STATE_KEY] && window[STATE_KEY].cleanup) window[STATE_KEY].cleanup();
window[STATE_KEY] = {
currentIndex: -1,
cleanup: null,
updateTimeout: null
};
function updateState(targetIndex) {
window[STATE_KEY].currentIndex = targetIndex;
if (targetIndex === -1) {
View.applyHighlights(container, null);
View.renderTopBar(-1, container);
View.renderBottomBar(-1, container);
return;
}
const familyIndices = DataModel.getFamilyIndices(targetIndex);
View.applyHighlights(container, familyIndices);
View.renderTopBar(targetIndex, container);
View.renderBottomBar(targetIndex, container);
}
function onPointerOver(e) {
if (!container.contains(e.target)) return;
try {
const pos = client.editorView.posAtCoords({x: e.clientX, y: e.clientY});
if (pos != null) {
const idx = DataModel.findHeadingIndexByPos(pos);
if (idx !== window[STATE_KEY].currentIndex || !document.querySelector(".sb-active")) {
updateState(idx);
}
}
} catch (err) { }
}
function onCursorActivity(e) {
if (window[STATE_KEY].updateTimeout) clearTimeout(window[STATE_KEY].updateTimeout);
window[STATE_KEY].updateTimeout = setTimeout(() => {
try {
const state = client.editorView.state;
const pos = state.selection.main.head;
const idx = DataModel.findHeadingIndexByPos(pos);
updateState(idx);
} catch (e) {}
}, 50);
}
let isScrolling = false;
function handleScroll() {
if (container.matches(":hover")) {
isScrolling = false;
return;
}
const viewportTopPos = client.editorView.viewport.from;
const idx = DataModel.findHeadingIndexByPos(viewportTopPos + 50);
updateState(idx);
isScrolling = false;
}
function onScroll() {
if (!isScrolling) {
window.requestAnimationFrame(handleScroll);
isScrolling = true;
}
}
const mo = new MutationObserver((mutations) => {
if (window[STATE_KEY].currentIndex !== -1) {
const activeEl = container.querySelector(".sb-active");
if (!activeEl) {
const familyIndices = DataModel.getFamilyIndices(window[STATE_KEY].currentIndex);
View.applyHighlights(container, familyIndices);
}
}
});
mo.observe(container, { childList: true, subtree: true, attributes: false });
container.addEventListener("pointerover", onPointerOver);
container.addEventListener("click", onCursorActivity);
container.addEventListener("keyup", onCursorActivity);
window.addEventListener("scroll", onScroll, { passive: true });
window[STATE_KEY].cleanup = () => {
container.removeEventListener("pointerover", onPointerOver);
container.removeEventListener("click", onCursorActivity);
container.removeEventListener("keyup", onCursorActivity);
window.removeEventListener("scroll", onScroll);
mo.disconnect();
if (window[STATE_KEY].updateTimeout) clearTimeout(window[STATE_KEY].updateTimeout);
View.applyHighlights(container, null);
const top = document.getElementById(View.topContainerId);
const bot = document.getElementById(View.bottomContainerId);
if (top) top.remove();
if (bot) bot.remove();
DataModel.headings = [];
};
console.log("[HHH] v13 Enabled");
};
bind();
}
export function disableHighlight() {
if (window[STATE_KEY] && window[STATE_KEY].cleanup) {
window[STATE_KEY].cleanup();
window[STATE_KEY] = null;
}
}
]]
command.define {
name = "Save: HHH.js",
hide = true,
run = function()
local jsFile = space.writeDocument("Library/xczphysics/STYLE/Theme/HHH.js", jsCode)
editor.flashNotification("HHH JS saved (" .. jsFile.size .. " bytes)")
end
}
command.define {
name = "Delete: HHH.js",
hide = true,
run = function()
space.deleteDocument("Library/xczphysics/STYLE/Theme/HHH.js")
editor.flashNotification("JS-File deleted")
end
}
command.define {
name = "Enable: HierarchyHighlightHeadings",
run = function()
js.import("/.fs/Library/xczphysics/STYLE/Theme/HHH.js").enableHighlight()
end
}
command.define {
name = "Disable HierarchyHighlightHeadings",
run = function()
js.import("/.fs/Library/xczphysics/STYLE/Theme/HHH.js").disableHighlight()
end
}
event.listen from CONFIG/Edit/Read_Only_Toggle-- event.listen {
-- name = 'system:ready',
-- run = function(e)
-- js.import("/.fs/Library/xczphysics/STYLE/Theme/HHH.js").enableHighlight()
-- end
-- }
js.import("/.fs/Library/xczphysics/STYLE/Theme/HHH.js").enableHighlight()
/* =========================================
统一主题样式 - HHH + LinkFloater v4
========================================= */
/* =========================================
1. 共享变量
========================================= */
:root {
/* Dark theme colors */
--h1-color-dark: #e6c8ff;
--h2-color-dark: #a0d8ff;
--h3-color-dark: #98ffb3;
--h4-color-dark: #fff3a8;
--h5-color-dark: #ffb48c;
--h6-color-dark: #ffa8ff;
/* Light theme colors */
--h1-color-light: #6b2e8c;
--h2-color-light: #1c4e8b;
--h3-color-light: #1a6644;
--h4-color-light: #a67c00;
--h5-color-light: #b84c1c;
--h6-color-light: #993399;
--title-opacity: 0.5;
/* LinkFloater colors */
--link-local-color: #4caf50;
--link-remote-color: #2196f3;
--link-backlink-color: #ff9800;
}
/* =========================================
2. 共享基础样式
========================================= */
.sb-frozen-item,
.sb-floater-btn {
display: inline-block;
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: auto;
cursor: pointer;
margin: 0;
padding: 0.2em 0.6em;
border-radius: 4px;
box-sizing: border-box;
opacity: 0.8;
background-color: var(--bg-color, #ffffff);
/* 完整边框(透明) */
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-family: inherit;
transition: all 0.15s ease-out;
}
/* 悬浮效果 - 包含完整边框(顶边和底边) */
.sb-frozen-item:hover,
.sb-frozen-item.sb-frozen-expanded,
.sb-floater-btn:hover,
.sb-floater-btn.sb-floater-expanded {
opacity: 1;
z-index: 1001;
max-width: none;
filter: brightness(0.95) contrast(0.95);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
/* 完整边框高亮 */
border: 1px solid currentColor;
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
.sb-frozen-item,
.sb-floater-btn {
background-color: var(--bg-color-dark, #252629);
border-color: rgba(255, 255, 255, 0.1);
}
.sb-frozen-item:hover,
.sb-floater-btn:hover {
background-color: #333;
filter: brightness(1.2);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
border: 1px solid currentColor;
}
}
html[data-theme="dark"] .sb-frozen-item,
html[data-theme="dark"] .sb-floater-btn {
background-color: var(--bg-color-dark, #252629);
border-color: rgba(255, 255, 255, 0.1);
color: #ccc;
}
html[data-theme="dark"] .sb-frozen-item:hover,
html[data-theme="dark"] .sb-floater-btn:hover {
background-color: #333;
filter: brightness(1.2);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
border: 1px solid currentColor;
}
/* =========================================
3. HHH 专用样式
========================================= */
#sb-frozen-container-top,
#sb-frozen-container-bottom {
display: flex;
flex-direction: row;
gap: 8px;
align-items: flex-start;
pointer-events: none;
}
.sb-frozen-col {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
pointer-events: auto;
}
.sb-frozen-header {
font-size: 10px;
opacity: 0.5;
margin-bottom: 2px;
pointer-events: none;
}
/* 标题项容器(包含树状连接、按钮、层级标识) */
.sb-frozen-item-wrapper {
display: flex;
align-items: flex-end;
gap: 4px;
}
/* HHH 专用:宽度限制 */
.sb-frozen-item {
max-width: 40vw;
}
.sb-frozen-item:hover {
transform: translateY(-1px);
}
/* 树状连接线 */
.sb-frozen-tree {
font-family: monospace;
font-size: 11px;
opacity: 0.4;
color: currentColor;
white-space: pre;
line-height: 1.2;
pointer-events: none;
user-select: none;
}
.sb-frozen-item-wrapper:hover .sb-frozen-tree {
opacity: 0.8;
}
/* 层级标识 (H1-H6) */
.sb-frozen-level {
font-size: 9px;
font-weight: bold;
opacity: 0.4;
padding: 1px 3px;
border-radius: 2px;
background-color: currentColor;
color: var(--bg-color, #fff);
line-height: 1;
pointer-events: none;
user-select: none;
margin-left: -2px;
align-self: flex-end;
}
/* 层级标识悬浮高亮 */
.sb-frozen-item-wrapper:hover .sb-frozen-level {
opacity: 0.7;
}
/* HHH 层级指示器 */
.sb-frozen-level-indicator {
position: absolute;
left: 2px;
bottom: 0px;
font-size: 8px;
opacity: 0.4;
font-family: monospace;
pointer-events: none;
transition: opacity 0.15s;
}
.sb-frozen-item:hover .sb-frozen-level-indicator,
.sb-frozen-item.sb-frozen-expanded .sb-frozen-level-indicator {
opacity: 0.8;
}
/* 树状结构前缀 */
.sb-frozen-tree-prefix {
color: var(--secondary-text-color, #888);
user-select: none;
}
.sb-frozen-item-wrapper:hover .sb-frozen-tree-prefix {
opacity: 0.8;
}
/* 层级标识颜色反转 */
html[data-theme="dark"] .sb-frozen-level {
color: #1a1a1a;
}
html[data-theme="light"] .sb-frozen-level {
color: #fff;
}
/* 标题颜色 */
html[data-theme="dark"] .sb-frozen-l1,
html[data-theme="dark"] .sb-frozen-item-wrapper.sb-frozen-l1 { color: var(--h1-color-dark); }
html[data-theme="dark"] .sb-frozen-l2,
html[data-theme="dark"] .sb-frozen-item-wrapper.sb-frozen-l2 { color: var(--h2-color-dark); }
html[data-theme="dark"] .sb-frozen-l3,
html[data-theme="dark"] .sb-frozen-item-wrapper.sb-frozen-l3 { color: var(--h3-color-dark); }
html[data-theme="dark"] .sb-frozen-l4,
html[data-theme="dark"] .sb-frozen-item-wrapper.sb-frozen-l4 { color: var(--h4-color-dark); }
html[data-theme="dark"] .sb-frozen-l5,
html[data-theme="dark"] .sb-frozen-item-wrapper.sb-frozen-l5 { color: var(--h5-color-dark); }
html[data-theme="dark"] .sb-frozen-l6,
html[data-theme="dark"] .sb-frozen-item-wrapper.sb-frozen-l6 { color: var(--h6-color-dark); }
html[data-theme="light"] .sb-frozen-l1,
html[data-theme="light"] .sb-frozen-item-wrapper.sb-frozen-l1 { color: var(--h1-color-light); }
html[data-theme="light"] .sb-frozen-l2,
html[data-theme="light"] .sb-frozen-item-wrapper.sb-frozen-l2 { color: var(--h2-color-light); }
html[data-theme="light"] .sb-frozen-l3,
html[data-theme="light"] .sb-frozen-item-wrapper.sb-frozen-l3 { color: var(--h3-color-light); }
html[data-theme="light"] .sb-frozen-l4,
html[data-theme="light"] .sb-frozen-item-wrapper.sb-frozen-l4 { color: var(--h4-color-light); }
html[data-theme="light"] .sb-frozen-l5,
html[data-theme="light"] .sb-frozen-item-wrapper.sb-frozen-l5 { color: var(--h5-color-light); }
html[data-theme="light"] .sb-frozen-l6,
html[data-theme="light"] .sb-frozen-item-wrapper.sb-frozen-l6 { color: var(--h6-color-light); }
/* 编辑器内标题样式 */
.sb-line-h1, .sb-line-h2, .sb-line-h3,
.sb-line-h4, .sb-line-h5, .sb-line-h6 {
position: relative;
opacity: var(--title-opacity);
border-bottom: none !important;
background-image: linear-gradient(90deg, currentColor, transparent);
background-size: 100% 2px;
background-position: 0 100%;
background-repeat: no-repeat;
transition: opacity 0.15s;
}
html[data-theme="dark"] .sb-line-h1 { font-size:1.8em !important; color:var(--h1-color-dark)!important; }
html[data-theme="dark"] .sb-line-h2 { font-size:1.6em !important; color:var(--h2-color-dark)!important; }
html[data-theme="dark"] .sb-line-h3 { font-size:1.4em !important; color:var(--h3-color-dark)!important; }
html[data-theme="dark"] .sb-line-h4 { font-size:1.2em !important; color:var(--h4-color-dark)!important; }
html[data-theme="dark"] .sb-line-h5 { font-size:1em !important; color:var(--h5-color-dark)!important; }
html[data-theme="dark"] .sb-line-h6 { font-size:1em !important; color:var(--h6-color-dark)!important; }
html[data-theme="light"] .sb-line-h1 { font-size:1.8em !important; color:var(--h1-color-light)!important; }
html[data-theme="light"] .sb-line-h2 { font-size:1.6em !important; color:var(--h2-color-light)!important; }
html[data-theme="light"] .sb-line-h3 { font-size:1.4em !important; color:var(--h3-color-light)!important; }
html[data-theme="light"] .sb-line-h4 { font-size:1.2em !important; color:var(--h4-color-light)!important; }
html[data-theme="light"] .sb-line-h5 { font-size:1em !important; color:var(--h5-color-light)!important; }
html[data-theme="light"] .sb-line-h6 { font-size:1em !important; color:var(--h6-color-light)!important; }
/* 高亮状态 */
.sb-active { opacity: 1 !important; }
.sb-active::before {
content: "";
position: absolute;
top: -2px;
left: -4px;
right: -4px;
bottom: 0;
background-color: currentColor;
opacity: 0.15;
z-index: -1;
pointer-events: none;
border-radius: 4px;
}
/* =========================================
4. LinkFloater 专用样式
========================================= */
.sb-floater-container {
position: fixed;
right: 20px;
z-index: 1000;
pointer-events: none;
font-family: inherit;
font-size: 12px;
}
.sb-floater-wrapper {
display: flex;
flex-direction: row;
gap: 6px;
pointer-events: auto;
}
.sb-floater-col {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
pointer-events: auto;
}
.sb-floater-header {
font-size: 10px;
text-transform: uppercase;
color: var(--secondary-text-color, #888);
opacity: 0.6;
margin-bottom: 2px;
margin-right: 2px;
font-weight: bold;
pointer-events: none;
}
/* LinkFloater 专用:宽度限制 */
.sb-floater-btn {
max-width: 120px;
}
.sb-floater-btn:hover {
transform: translateX(-2px);
}
/* 链接类型颜色 + 左侧条 */
.sb-floater-local {
border-left: 3px solid var(--link-local-color);
color: var(--link-local-color);
}
.sb-floater-remote {
border-left: 3px solid var(--link-remote-color);
color: var(--link-remote-color);
font-weight: bold;
}
.sb-floater-backlink {
border-right: 3px solid var(--link-backlink-color);
border-left-width: 1px;
color: var(--link-backlink-color);
}
/* 悬浮时保持类型边框 + 完整边框 */
.sb-floater-local:hover {
border: 1px solid var(--link-local-color);
border-left: 3px solid var(--link-local-color);
}
.sb-floater-remote:hover {
border: 1px solid var(--link-remote-color);
border-left: 3px solid var(--link-remote-color);
}
.sb-floater-backlink:hover {
border: 1px solid var(--link-backlink-color);
border-right: 3px solid var(--link-backlink-color);
}
html[data-theme="dark"] .sb-floater-local {
color: #81c784;
border-left-color: #81c784;
}
html[data-theme="dark"] .sb-floater-remote {
color: #64b5f6;
border-left-color: #64b5f6;
}
html[data-theme="dark"] .sb-floater-backlink {
color: #ffb74d;
border-right-color: #ffb74d;
}
html[data-theme="dark"] .sb-floater-local:hover {
border-color: #81c784;
border-left: 3px solid #81c784;
}
html[data-theme="dark"] .sb-floater-remote:hover {
border-color: #64b5f6;
border-left: 3px solid #64b5f6;
}
html[data-theme="dark"] .sb-floater-backlink:hover {
border-color: #ffb74d;
border-right: 3px solid #ffb74d;
}