|
| 1 | +--- |
| 2 | +recommend: ⭐⭐⭐⭐⭐ |
| 3 | +--githubUrl: "https://github.com/ChenZhu-Xie/xczphysics_SilverBullet/blob/main/STYLE/Theme/HHH.md" |
| 4 | +udpateDate: 2025-10-28 |
| 5 | +---rawUrl: "https://raw.githubusercontent.com/ChenZhu-Xie/xczphysics_SilverBullet/main/STYLE/Theme/HHH.md" |
| 6 | +updateDate: "2025-11-01T13:24:02Z" |
| 7 | +lastCommitDate: "2025-10-31T09:45:25Z" |
| 8 | +--- |
| 9 | + |
| 10 | +# HierarchyHighlightHeadings - HHH Theme |
| 11 | + |
| 12 | +## JS Part (Experimental) |
| 13 | + |
| 14 | +### Step 1. Reload your space to load the space-lua from this page: ${widgets.commandButton("System: Reload")} |
| 15 | + |
| 16 | +### Step 2. Save Library/PanelDragResize.js using this button: ${widgets.commandButton("Save: HierarchyHighlightHeadings.js")} |
| 17 | + |
| 18 | +### Step 3. System Reload: ${widgets.commandButton("System: Reload")} |
| 19 | + |
| 20 | +### Step 4. Reload UI: ${widgets.commandButton("Client: Reload UI")} |
| 21 | + |
| 22 | +1. borrowed `JS inject` from [[CONFIG/View/Tree/Float]] |
| 23 | +2. https://community.silverbullet.md/t/hhh-hierarchyhighlightheadings-theme/3467 |
| 24 | + |
| 25 | +```space-lua |
| 26 | +local jsCode = [[ |
| 27 | +const STATE_KEY = "__xhHighlightState_v2"; |
| 28 | +
|
| 29 | +function getLevel(el) { |
| 30 | + for (let i = 1; i <= 6; i++) { |
| 31 | + if (el.classList && el.classList.contains(`sb-line-h${i}`)) return i; |
| 32 | + } |
| 33 | + const tag = el.tagName ? el.tagName.toLowerCase() : ""; |
| 34 | + if (/^h[1-6]$/.test(tag)) return Number(tag[1]); |
| 35 | + return 0; |
| 36 | +} |
| 37 | +
|
| 38 | +function pickGroupRoot(start, container, groupSelector) { |
| 39 | + if (!groupSelector) return container; |
| 40 | + const g = start.closest(groupSelector); |
| 41 | + return g || container; |
| 42 | +} |
| 43 | +
|
| 44 | +function listHeadings(root, headingSelector) { |
| 45 | + return Array.from(root.querySelectorAll(headingSelector)); |
| 46 | +} |
| 47 | +
|
| 48 | +function collectDescendants(startIndex, headings, startLevel) { |
| 49 | + const res = []; |
| 50 | + for (let i = startIndex + 1; i < headings.length; i++) { |
| 51 | + const lvl = getLevel(headings[i]); |
| 52 | + if (lvl <= startLevel) break; |
| 53 | + res.push(headings[i]); |
| 54 | + } |
| 55 | + return res; |
| 56 | +} |
| 57 | +
|
| 58 | +function collectAncestors(startIndex, headings, startLevel) { |
| 59 | + const res = []; |
| 60 | + let minLevel = startLevel; |
| 61 | + for (let i = startIndex - 1; i >= 0; i--) { |
| 62 | + const lvl = getLevel(headings[i]); |
| 63 | + if (lvl < minLevel) { |
| 64 | + res.push(headings[i]); |
| 65 | + minLevel = lvl; |
| 66 | + if (minLevel === 1) break; |
| 67 | + } |
| 68 | + } |
| 69 | + return res; |
| 70 | +} |
| 71 | +
|
| 72 | +function clearClasses(root) { |
| 73 | + root.querySelectorAll(".sb-active, .sb-active-anc, .sb-active-desc, .sb-active-current") |
| 74 | + .forEach(el => el.classList.remove("sb-active", "sb-active-anc", "sb-active-desc", "sb-active-current")); |
| 75 | +} |
| 76 | +
|
| 77 | +export function enableHighlight(opts = {}) { |
| 78 | + const containerSelector = opts.containerSelector || "#sb-main"; |
| 79 | + const headingSelector = opts.headingSelector || |
| 80 | + "h1, h2, h3, h4, h5, h6, .sb-line-h1, .sb-line-h2, .sb-line-h3, .sb-line-h4, .sb-line-h5, .sb-line-h6"; |
| 81 | + const groupSelector = opts.groupSelector || ".sb-title-group"; |
| 82 | + const debug = !!opts.debug; |
| 83 | +
|
| 84 | + const bind = () => { |
| 85 | + const container = document.querySelector(containerSelector); |
| 86 | + if (!container) { requestAnimationFrame(bind); return; } |
| 87 | +
|
| 88 | + const prev = window[STATE_KEY]; |
| 89 | + if (prev && prev.cleanup) prev.cleanup(); |
| 90 | +
|
| 91 | + function onPointerOver(e) { |
| 92 | + const h = e.target && e.target.closest && e.target.closest(headingSelector); |
| 93 | + if (!h || !container.contains(h)) return; |
| 94 | +
|
| 95 | + const groupRoot = pickGroupRoot(h, container, groupSelector); |
| 96 | + const headings = listHeadings(groupRoot, headingSelector); |
| 97 | + const startIndex = headings.indexOf(h); |
| 98 | + if (startIndex === -1) return; |
| 99 | +
|
| 100 | + clearClasses(container); |
| 101 | +
|
| 102 | + const startLevel = getLevel(h); |
| 103 | + const descendants = collectDescendants(startIndex, headings, startLevel); |
| 104 | + const ancestors = collectAncestors(startIndex, headings, startLevel); |
| 105 | +
|
| 106 | + h.classList.add("sb-active", "sb-active-current"); |
| 107 | + descendants.forEach(el => el.classList.add("sb-active", "sb-active-desc")); |
| 108 | + ancestors.forEach(el => el.classList.add("sb-active", "sb-active-anc")); |
| 109 | +
|
| 110 | + if (debug) { |
| 111 | + console.log( |
| 112 | + "[HHH] level", startLevel, |
| 113 | + "anc", ancestors.length, |
| 114 | + "desc", descendants.length, |
| 115 | + "text:", (h.textContent || "").trim().slice(0, 60) |
| 116 | + ); |
| 117 | + } |
| 118 | + } |
| 119 | +
|
| 120 | + function onPointerOut(e) { |
| 121 | + const from = e.target && e.target.closest && e.target.closest(headingSelector); |
| 122 | + const to = e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(headingSelector); |
| 123 | + if (from && (!to || !container.contains(to))) { |
| 124 | + clearClasses(container); |
| 125 | + } |
| 126 | + } |
| 127 | +
|
| 128 | + function onPointerLeave() { |
| 129 | + clearClasses(container); |
| 130 | + } |
| 131 | +
|
| 132 | + container.addEventListener("pointerover", onPointerOver); |
| 133 | + container.addEventListener("pointerout", onPointerOut); |
| 134 | + container.addEventListener("pointerleave", onPointerLeave); |
| 135 | +
|
| 136 | + const mo = new MutationObserver(() => { clearClasses(container); }); |
| 137 | + mo.observe(container, { childList: true, subtree: true }); |
| 138 | +
|
| 139 | + window[STATE_KEY] = { |
| 140 | + cleanup() { |
| 141 | + try { |
| 142 | + container.removeEventListener("pointerover", onPointerOver); |
| 143 | + container.removeEventListener("pointerout", onPointerOut); |
| 144 | + container.removeEventListener("pointerleave", onPointerLeave); |
| 145 | + } catch {} |
| 146 | + try { mo.disconnect(); } catch {} |
| 147 | + clearClasses(container); |
| 148 | + } |
| 149 | + }; |
| 150 | +
|
| 151 | + if (debug) console.log("[HHH] enabled"); |
| 152 | + }; |
| 153 | +
|
| 154 | + bind(); |
| 155 | +} |
| 156 | +
|
| 157 | +export function disableHighlight() { |
| 158 | + const st = window[STATE_KEY]; |
| 159 | + if (st && st.cleanup) st.cleanup(); |
| 160 | + window[STATE_KEY] = null; |
| 161 | +} |
| 162 | +]] |
| 163 | +
|
| 164 | +command.define { |
| 165 | + name = "Save: HierarchyHighlightHeadings.js", |
| 166 | + hide = true, |
| 167 | + run = function() |
| 168 | + local jsFile = space.writeDocument("Library/HierarchyHighlightHeadings.js", jsCode) |
| 169 | + editor.flashNotification("HierarchyHighlightHeadings JS saved with size: " .. jsFile.size .. " bytes") |
| 170 | + end |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +```space-lua |
| 175 | +command.define { |
| 176 | + name = "Enable: HierarchyHighlightHeadings", |
| 177 | + run = function() |
| 178 | + js.import("/.fs/Library/HierarchyHighlightHeadings.js").enableHighlight() |
| 179 | + end |
| 180 | +} |
| 181 | +
|
| 182 | +command.define { |
| 183 | + name = "Disable HierarchyHighlightHeadings", |
| 184 | + hide = true, |
| 185 | + run = function() |
| 186 | + js.import("/.fs/Library/HierarchyHighlightHeadings.js").disableHighlight() |
| 187 | + end |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +1. borrowed `event.listen` from [[CONFIG/Edit/Read Only Toggle]] |
| 192 | + |
| 193 | +```space-lua |
| 194 | +event.listen { |
| 195 | + name = 'system:ready', |
| 196 | + run = function(e) |
| 197 | + js.import("/.fs/Library/HierarchyHighlightHeadings.js").enableHighlight() |
| 198 | + end |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +## CSS part |
| 203 | + |
| 204 | +1. Remember to Cancel the `1st space-style block` from [[STYLE/Theme/HHH-css]] |
| 205 | + |
| 206 | +```space-style |
| 207 | +/* 默认半透明 */ |
| 208 | +.sb-line-h1, .sb-line-h2, .sb-line-h3, |
| 209 | +.sb-line-h4, .sb-line-h5, .sb-line-h6 { |
| 210 | + opacity: var(--title-opacity); |
| 211 | + /* transition: opacity 0.2s; */ |
| 212 | +} |
| 213 | +
|
| 214 | +/* 标题自身 hover 可高亮该标题 */ |
| 215 | +.sb-line-h1:hover, |
| 216 | +.sb-line-h2:hover, |
| 217 | +.sb-line-h3:hover, |
| 218 | +.sb-line-h4:hover, |
| 219 | +.sb-line-h5:hover, |
| 220 | +.sb-line-h6:hover { |
| 221 | + opacity: 1 !important; |
| 222 | +} |
| 223 | +
|
| 224 | +/* 仅保留 JS 驱动的高亮 */ |
| 225 | +.sb-active { |
| 226 | + opacity: 1 !important; |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +1. https://chatgpt.com/share/68fd0e6f-19d8-8010-95b8-c0f80a829e9b |
| 231 | + |
| 232 | +```space-style |
| 233 | +:root { |
| 234 | + /* Dark theme 颜色变量 */ |
| 235 | + --h1-color-dark: #ee82ee; |
| 236 | + --h2-color-dark: #6a5acd; |
| 237 | + --h3-color-dark: #4169e1; |
| 238 | + --h4-color-dark: #008000; |
| 239 | + --h5-color-dark: #ffff00; |
| 240 | + --h6-color-dark: #ffa500; |
| 241 | +
|
| 242 | + --h1-underline-dark: rgba(230,200,255,0.3); |
| 243 | + --h2-underline-dark: rgba(160,216,255,0.3); |
| 244 | + --h3-underline-dark: rgba(152,255,179,0.3); |
| 245 | + --h4-underline-dark: rgba(255,243,168,0.3); |
| 246 | + --h5-underline-dark: rgba(255,180,140,0.3); |
| 247 | + --h6-underline-dark: rgba(255,168,255,0.3); |
| 248 | +
|
| 249 | + |
| 250 | + /* Light theme 颜色变量 */ |
| 251 | + --h1-color-light: #6b2e8c; |
| 252 | + --h2-color-light: #1c4e8b; |
| 253 | + --h3-color-light: #1a6644; |
| 254 | + --h4-color-light: #a67c00; |
| 255 | + --h5-color-light: #b84c1c; |
| 256 | + --h6-color-light: #993399; |
| 257 | +
|
| 258 | + --h1-underline-light: rgba(107,46,140,0.3); |
| 259 | + --h2-underline-light: rgba(28,78,139,0.3); |
| 260 | + --h3-underline-light: rgba(26,102,68,0.3); |
| 261 | + --h4-underline-light: rgba(166,124,0,0.3); |
| 262 | + --h5-underline-light: rgba(184,76,28,0.3); |
| 263 | + --h6-underline-light: rgba(153,51,153,0.3); |
| 264 | +
|
| 265 | + --title-opacity: 0.9; |
| 266 | +} |
| 267 | +
|
| 268 | +/* 公共 H1–H6 样式 */ |
| 269 | +.sb-line-h1, .sb-line-h2, .sb-line-h3, |
| 270 | +.sb-line-h4, .sb-line-h5, .sb-line-h6 { |
| 271 | + position: relative; |
| 272 | + opacity: var(--title-opacity); |
| 273 | + border-bottom-style: solid; |
| 274 | + border-bottom-width: 2px; |
| 275 | + border-image-slice: 1; |
| 276 | +} |
| 277 | +
|
| 278 | +/* Dark Theme */ |
| 279 | +html[data-theme="dark"] { |
| 280 | + .sb-line-h1 { font-size:1.8em !important; color:var(--h1-color-dark)!important; border-bottom: 2px solid var(--h1-underline-dark); } |
| 281 | + .sb-line-h2 { font-size:1.6em !important; color:var(--h2-color-dark)!important; border-bottom: 2px solid var(--h2-underline-dark); } |
| 282 | + .sb-line-h3 { font-size:1.4em !important; color:var(--h3-color-dark)!important; border-bottom: 2px solid var(--h3-underline-dark); } |
| 283 | + .sb-line-h4 { font-size:1.2em !important; color:var(--h4-color-dark)!important; border-bottom: 2px solid var(--h4-underline-dark); } |
| 284 | + .sb-line-h5 { font-size:1em !important; color:var(--h5-color-dark)!important; border-bottom: 2px solid var(--h5-underline-dark); } |
| 285 | + .sb-line-h6 { font-size:1em !important; color:var(--h6-color-dark)!important; border-bottom: 2px solid var(--h6-underline-dark); } |
| 286 | +} |
| 287 | +
|
| 288 | +/* Light Theme */ |
| 289 | +html[data-theme="light"] { |
| 290 | + .sb-line-h1 { font-size:1.8em !important; color:var(--h1-color-light)!important; border-bottom: 2px solid var(--h1-underline-light); } |
| 291 | + .sb-line-h2 { font-size:1.6em !important; color:var(--h2-color-light)!important; border-bottom: 2px solid var(--h2-underline-light); } |
| 292 | + .sb-line-h3 { font-size:1.4em !important; color:var(--h3-color-light)!important; border-bottom: 2px solid var(--h3-underline-light); } |
| 293 | + .sb-line-h4 { font-size:1.2em !important; color:var(--h4-color-light)!important; border-bottom: 2px solid var(--h4-underline-light); } |
| 294 | + .sb-line-h5 { font-size:1em !important; color:var(--h5-color-light)!important; border-bottom: 2px solid var(--h5-underline-light); } |
| 295 | + .sb-line-h6 { font-size:1em !important; color:var(--h6-color-light)!important; border-bottom: 2px solid var(--h6-underline-light); } |
| 296 | +} |
| 297 | +
|
| 298 | +/* 高亮类 */ |
| 299 | +.sb-active { |
| 300 | + opacity: 1 !important; |
| 301 | +} |
| 302 | +``` |
| 303 | + |
| 304 | +```space-style |
| 305 | +
|
| 306 | +:root { |
| 307 | + --h-bg-alpha-dark: 20%; /* 深色主题 */ |
| 308 | + --h-bg-alpha-light: 8%; /* 浅色主题 */ |
| 309 | +} |
| 310 | +
|
| 311 | +/* 深色主题:hover 或 .sb-active 才上很淡背景 */ |
| 312 | +html[data-theme="dark"] .sb-line-h1:hover, |
| 313 | +html[data-theme="dark"] .sb-line-h2:hover, |
| 314 | +html[data-theme="dark"] .sb-line-h3:hover, |
| 315 | +html[data-theme="dark"] .sb-line-h4:hover, |
| 316 | +html[data-theme="dark"] .sb-line-h5:hover, |
| 317 | +html[data-theme="dark"] .sb-line-h6:hover, |
| 318 | +html[data-theme="dark"] .sb-active { |
| 319 | + background-color: color-mix(in srgb, currentColor var(--h-bg-alpha-dark), transparent); |
| 320 | +} |
| 321 | +
|
| 322 | +/* 浅色主题:hover 或 .sb-active 才上很淡背景 */ |
| 323 | +html[data-theme="light"] .sb-line-h1:hover, |
| 324 | +html[data-theme="light"] .sb-line-h2:hover, |
| 325 | +html[data-theme="light"] .sb-line-h3:hover, |
| 326 | +html[data-theme="light"] .sb-line-h4:hover, |
| 327 | +html[data-theme="light"] .sb-line-h5:hover, |
| 328 | +html[data-theme="light"] .sb-line-h6:hover, |
| 329 | +html[data-theme="light"] .sb-active { |
| 330 | + background-color: color-mix(in srgb, currentColor var(--h-bg-alpha-light), transparent); |
| 331 | +} |
| 332 | +
|
| 333 | +/* 深色:只在 hover 或 sb-active 时给标题行一个很淡的同色背景 */ |
| 334 | +html[data-theme="dark"] :is(.sb-line-h1,.sb-line-h2,.sb-line-h3,.sb-line-h4,.sb-line-h5,.sb-line-h6):is(:hover,.sb-active) { |
| 335 | + background-color: color-mix(in srgb, currentColor var(--h-bg-alpha-dark), transparent); |
| 336 | +} |
| 337 | +
|
| 338 | +/* 浅色:同理 */ |
| 339 | +html[data-theme="light"] :is(.sb-line-h1,.sb-line-h2,.sb-line-h3,.sb-line-h4,.sb-line-h5,.sb-line-h6):is(:hover,.sb-active) { |
| 340 | + background-color: color-mix(in srgb, currentColor var(--h-bg-alpha-light), transparent); |
| 341 | +} |
| 342 | +``` |
0 commit comments