diff --git a/tools/StaticBlogGenerator.js b/tools/StaticBlogGenerator.js index 7aea770..2120606 100644 --- a/tools/StaticBlogGenerator.js +++ b/tools/StaticBlogGenerator.js @@ -133,6 +133,7 @@ class StaticBlogGenerator { '', ' ', ' ', + ' ', ` ${escapedTitle}`, ' ', ` `, @@ -150,7 +151,10 @@ class StaticBlogGenerator { ' ', '', '', + '', + this.renderScrollTopBar(), bodyHtml, ' ', '', @@ -163,13 +167,27 @@ class StaticBlogGenerator { */ renderHeaderLinks() { return [ - '' + ' materials' + ].join('\n'); + } + + /** + * @returns {string} + */ + renderScrollTopBar() { + return [ + '
', + ' ', + ' ', + ' ', + ' ', + '
' ].join('\n'); } diff --git a/website/blog.html b/website/blog.html index ffa4f0c..bb9dddf 100644 --- a/website/blog.html +++ b/website/blog.html @@ -3,6 +3,7 @@ + Blog @@ -27,6 +28,18 @@ bluesky materials +
+ + + + +
diff --git a/website/blog/markdown-test/index.html b/website/blog/markdown-test/index.html index 5c50374..7e471c9 100644 --- a/website/blog/markdown-test/index.html +++ b/website/blog/markdown-test/index.html @@ -3,6 +3,7 @@ + Markdown Formatting Test - Blog @@ -27,6 +28,18 @@ bluesky materials +
+ + + + +
diff --git a/website/blog/node-graphs/index.html b/website/blog/node-graphs/index.html index 6b892c7..b57a2d8 100644 --- a/website/blog/node-graphs/index.html +++ b/website/blog/node-graphs/index.html @@ -3,6 +3,7 @@ + Building Interactive Node Graphs - Blog @@ -27,6 +28,18 @@ bluesky materials +
+ + + + +
diff --git a/website/blog/testddd/index.html b/website/blog/testddd/index.html index 7470103..6ff52e4 100644 --- a/website/blog/testddd/index.html +++ b/website/blog/testddd/index.html @@ -3,6 +3,7 @@ + test d d d d - Blog @@ -27,6 +28,18 @@ bluesky materials +
+ + + + +
diff --git a/website/blog/welcome/index.html b/website/blog/welcome/index.html index 5bee298..0f72aca 100644 --- a/website/blog/welcome/index.html +++ b/website/blog/welcome/index.html @@ -3,6 +3,7 @@ + Welcome to My Blog - Blog @@ -27,6 +28,18 @@ bluesky materials +
+ + + + +
diff --git a/website/scripts/MarkdownRenderer.js b/website/scripts/MarkdownRenderer.js index b1590c3..1a12d13 100644 --- a/website/scripts/MarkdownRenderer.js +++ b/website/scripts/MarkdownRenderer.js @@ -45,8 +45,8 @@ export class MarkdownRenderer { return placeholder; }); - // Split into blocks separated by blank lines - const blocks = html.split(/\n{2,}/); + // Split into blocks, preserving loose-list continuity across blank lines + const blocks = MarkdownRenderer.#splitBlocks(html); const parts = blocks.map(block => { block = block.trim(); // Restore code blocks @@ -320,6 +320,100 @@ export class MarkdownRenderer { return Array.from({ length: lineCount }, () => "
").join("\n"); } + /** + * Splits markdown into renderable blocks while keeping loose/nested list items together. + * + * @param {string} markdown + * @returns {string[]} + */ + static #splitBlocks(markdown) { + const lines = markdown.split("\n"); + const blocks = []; + let index = 0; + + while (index < lines.length) { + while (index < lines.length && lines[index].trim() === "") { + index++; + } + + if (index >= lines.length) { + break; + } + + const line = lines[index]; + const trimmedLine = line.trim(); + + if (/^___CODEBLOCK_\d+___$/.test(trimmedLine)) { + blocks.push(trimmedLine); + index++; + continue; + } + + if (MarkdownRenderer.#isListMarkerLine(line)) { + const listLines = [line]; + index++; + + while (index < lines.length) { + const current = lines[index]; + + if (current.trim() === "") { + let lookAhead = index; + while (lookAhead < lines.length && lines[lookAhead].trim() === "") { + lookAhead++; + } + + if (lookAhead >= lines.length) { + index = lookAhead; + break; + } + + const next = lines[lookAhead]; + if (MarkdownRenderer.#isListMarkerLine(next) || /^\s+/.test(next)) { + listLines.push(...lines.slice(index, lookAhead)); + index = lookAhead; + continue; + } + + break; + } + + if (MarkdownRenderer.#isListMarkerLine(current) || /^\s+/.test(current)) { + listLines.push(current); + index++; + continue; + } + + break; + } + + blocks.push(listLines.join("\n").trimEnd()); + continue; + } + + const paragraphLines = [line]; + index++; + + while (index < lines.length && lines[index].trim() !== "") { + paragraphLines.push(lines[index]); + index++; + } + + blocks.push(paragraphLines.join("\n")); + } + + return blocks; + } + + /** + * Checks whether a line starts a markdown list item. + * + * @param {string} line + * @returns {boolean} + */ + static #isListMarkerLine(line) { + return /^\s*(?:[-*+]\s+|\d+\.\s+)/.test(line); + } + // ─── Inline-level ───────────────────────────────────────────────────────── /** diff --git a/website/scripts/blogPage.js b/website/scripts/blogPage.js index 184ea97..dcef1d9 100644 --- a/website/scripts/blogPage.js +++ b/website/scripts/blogPage.js @@ -65,8 +65,48 @@ function renderStaticBlogPost() { wireCopyButtons(target); } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", renderStaticBlogPost, { once: true }); -} else { - renderStaticBlogPost(); +/** + * Toggles the compact top bar when the hero logo scrolls out of view. + * + * @returns {void} + */ +function setupScrollTopBar() { + const body = document.body; + const bar = document.querySelector("[data-blog-scroll-topbar]"); + const heroLogo = document.querySelector(".blog-logo-link"); + + if (!bar) return; + + if (heroLogo && "IntersectionObserver" in window) { + const observer = new IntersectionObserver( + ([entry]) => { + body.classList.toggle("blog-scrolled", !entry.isIntersecting); + }, + { threshold: 0 } + ); + observer.observe(heroLogo); + } else { + // Fallback: fixed pixel threshold + const applyState = () => { + body.classList.toggle("blog-scrolled", window.scrollY >= 80); + }; + window.addEventListener("scroll", applyState, { passive: true }); + applyState(); + } +} + +/** + * Bootstraps static blog page behaviors. + * + * @returns {void} + */ +function initializeBlogPage() { + renderStaticBlogPost(); + setupScrollTopBar(); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializeBlogPage, { once: true }); +} else { + initializeBlogPage(); } diff --git a/website/styles/main.css b/website/styles/main.css index 72127e3..9e24eb3 100644 --- a/website/styles/main.css +++ b/website/styles/main.css @@ -593,6 +593,7 @@ body.blog-page { overflow-y: auto; overflow-x: hidden; display: block; + position: relative; background-image: linear-gradient(var(--grid-color-strong) 2px, transparent 2px), linear-gradient(90deg, var(--grid-color-strong) 2px, transparent 2px), @@ -604,15 +605,84 @@ body.blog-page { var(--grid-size) var(--grid-size), var(--grid-size) var(--grid-size); background-repeat: repeat; - background-attachment: scroll; + background-attachment: local; background-position: 0 0, 0 0, 0 0, 0 0; background-color: var(--bg); } +.blog-page, +.blog-page * { + user-select: text; +} + +.blog-page .quick-link, +.blog-page .blog-scroll-logo-link, +.blog-page .md-copy-btn, +.blog-page button { + user-select: none; +} + .blog-page .quick-links { position: fixed; - top: 20px; - left: 20px; + top: 0; + left: 0; + right: 0; + width: 100%; + min-height: 52px; + display: flex; + align-items: center; + gap: 16px; + padding: 10px 16px; + z-index: 620; +} + +.blog-scroll-topbar { + position: fixed; + top: -1px; + left: 0; + right: 0; + height: 44px; + display: flex; + align-items: center; + gap: 14px; + padding: 6px 14px; + background: var(--ctp-base); + border-bottom: 1px solid rgba(205, 214, 244, 0.15); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28); + transform: translateY(-110%); + opacity: 0; + pointer-events: none; + transition: transform 0.2s ease, opacity 0.2s ease; + z-index: 700; +} + +.blog-scroll-logo-link { + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; +} + +.blog-scroll-logo { + height: 28px; + width: auto; + display: block; +} + +.blog-scroll-links { + display: flex; + align-items: center; + gap: 14px; +} + +.blog-scroll-links .quick-link { + font-size: 12px; +} + +.blog-page.blog-scrolled .blog-scroll-topbar { + transform: translateY(0); + opacity: 1; + pointer-events: auto; } .blog-shell { @@ -622,7 +692,7 @@ body.blog-page { flex-direction: column; align-items: center; justify-content: center; - padding: 84px 16px 36px; + padding: 110px 16px 36px; } .blog-logo-link { @@ -644,11 +714,10 @@ body.blog-page { .blog-card { width: min(900px, 100%); - background: rgba(30, 30, 46, 0.92); + background: var(--ctp-base); border: 1px solid rgba(205, 214, 244, 0.14); border-radius: 16px; box-shadow: 0 20px 70px rgba(0, 0, 0, 0.35); - backdrop-filter: blur(8px); padding: 22px 24px 28px; } @@ -782,6 +851,13 @@ body.blog-page { margin: 0 auto; } +.blog-post-content .md-img--inline { + display: inline; + vertical-align: middle; + border-radius: 3px; + margin: 0 0.15em; +} + .blog-post-content .md-figcaption { margin-top: 0.45em; text-align: center; @@ -789,13 +865,68 @@ body.blog-page { font-size: 0.88rem; } +.blog-post-content .md-code-block { + position: relative; + margin: 0.8em 0; +} + +.blog-post-content .md-copy-btn { + position: absolute; + top: 0.4em; + right: 0.4em; + background: rgba(49, 50, 68, 0.6); + border: 1px solid rgba(205, 214, 244, 0.12); + border-radius: 4px; + padding: 0.25em 0.5em; + font-size: 0.9em; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s, background 0.15s; + z-index: 10; + user-select: none; +} + +.blog-post-content .md-code-block:hover .md-copy-btn { + opacity: 1; +} + +.blog-post-content .md-copy-btn:hover { + background: rgba(69, 71, 90, 0.8); +} + +.blog-post-content .md-copy-btn.copied { + opacity: 1; +} + +.blog-post-content .md-link { + color: rgba(137, 180, 250, 0.9); + text-decoration: underline; + text-underline-offset: 2px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.35em; + user-select: text; +} + +.blog-post-content .md-link:hover { + color: var(--ctp-text); +} + +.blog-post-content .md-link-icon { + width: 1em; + height: 1em; + flex-shrink: 0; + vertical-align: middle; +} + .blog-index-list { display: grid; gap: 12px; } .blog-index-item { - background: rgba(24, 24, 37, 0.65); + background: var(--ctp-mantle); border: 1px solid rgba(205, 214, 244, 0.12); border-radius: 12px; padding: 14px 14px; @@ -811,12 +942,10 @@ body.blog-page { } @media (max-width: 820px) { - .blog-page .quick-links { - position: static; - padding: 18px 16px 0; - margin-bottom: -54px; - justify-content: center; - z-index: 1; + .blog-scroll-topbar { + gap: 10px; + overflow-x: auto; + white-space: nowrap; } .blog-shell { @@ -2688,3 +2817,4 @@ body.blog-page { .node-shake-red { animation: node-shake-red 0.4s ease-in-out; } +