diff --git a/src/components/blog/PostBodyClient.tsx b/src/components/blog/PostBodyClient.tsx index 3fa5439..3d46b54 100644 --- a/src/components/blog/PostBodyClient.tsx +++ b/src/components/blog/PostBodyClient.tsx @@ -96,7 +96,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { let contentRef: HTMLDivElement | undefined; const [hljs, setHljs] = createSignal(null); - const addCopyButtons = () => { + const processCodeBlocks = () => { if (!contentRef) return; const codeBlocks = contentRef.querySelectorAll("pre code"); @@ -105,17 +105,51 @@ export default function PostBodyClient(props: PostBodyClientProps) { const pre = codeBlock.parentElement; if (!pre || pre.querySelector(".copy-button")) return; - // Create wrapper for positioning - pre.style.position = "relative"; + // Extract language from code block classes + const classes = Array.from(codeBlock.classList); + const languageClass = classes.find((cls) => cls.startsWith("language-")); + const language = languageClass?.replace("language-", "") || ""; + + // Create language header if language is detected and not already present + if ( + language && + pre.previousElementSibling?.classList.contains("language-header") === + false + ) { + const languageHeader = document.createElement("div"); + languageHeader.className = "language-header"; + languageHeader.textContent = language; + + // Insert header before pre element + pre.parentElement?.insertBefore(languageHeader, pre); + } + + // Add line numbers + const codeText = codeBlock.textContent || ""; + const lines = codeText.split("\n"); + const lineCount = + lines[lines.length - 1] === "" ? lines.length - 1 : lines.length; + + if (lineCount > 0 && !pre.querySelector(".line-numbers")) { + // Create line numbers container + const lineNumbers = document.createElement("div"); + lineNumbers.className = "line-numbers"; + + // Generate line numbers + for (let i = 1; i <= lineCount; i++) { + const lineNum = document.createElement("div"); + lineNum.textContent = i.toString(); + lineNumbers.appendChild(lineNum); + } + + pre.appendChild(lineNumbers); + } // Create copy button const copyButton = document.createElement("button"); - copyButton.className = - "copy-button absolute top-2 right-2 px-3 py-1.5 text-xs font-medium rounded transition-all duration-200 z-10"; - copyButton.style.cssText = - "background-color: var(--color-surface0); color: var(--color-text); border: 1px solid var(--color-overlay0);"; + copyButton.className = "copy-button"; copyButton.textContent = "Copy"; - copyButton.dataset.codeBlock = "true"; // Mark for event delegation + copyButton.dataset.codeBlock = "true"; pre.appendChild(copyButton); }); @@ -162,11 +196,18 @@ export default function PostBodyClient(props: PostBodyClientProps) { } }); + // Look for the references section marker to get the custom heading name + const marker = contentRef.querySelector( + "span[id='references-section-start']" + ) as HTMLElement | null; + const referencesHeadingText = + marker?.getAttribute("data-heading") || "References"; + const headings = contentRef.querySelectorAll("h2"); let referencesSection: HTMLElement | null = null; headings.forEach((heading) => { - if (heading.textContent?.trim() === "References") { + if (heading.textContent?.trim() === referencesHeadingText) { referencesSection = heading; } }); @@ -278,7 +319,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { if (hljsInstance && props.hasCodeBlock && contentRef) { setTimeout(() => { hljsInstance.highlightAll(); - addCopyButtons(); + processCodeBlocks(); }, 100); } }); @@ -288,7 +329,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { setTimeout(() => { processReferences(); if (props.hasCodeBlock) { - addCopyButtons(); + processCodeBlocks(); } }, 150); @@ -297,21 +338,6 @@ export default function PostBodyClient(props: PostBodyClientProps) { const handleCopyButtonInteraction = async (e: Event) => { const target = e.target as HTMLElement; - // Handle mouseenter - if ( - e.type === "mouseover" && - target.classList.contains("copy-button") - ) { - target.style.backgroundColor = "var(--color-surface1)"; - } - - // Handle mouseleave - if (e.type === "mouseout" && target.classList.contains("copy-button")) { - if (target.textContent === "Copy") { - target.style.backgroundColor = "var(--color-surface0)"; - } - } - // Handle click if (e.type === "click" && target.classList.contains("copy-button")) { const pre = target.parentElement; @@ -323,22 +349,20 @@ export default function PostBodyClient(props: PostBodyClientProps) { try { await navigator.clipboard.writeText(code); target.textContent = "Copied!"; - target.style.backgroundColor = "var(--color-green)"; - target.style.color = "var(--color-base)"; + target.classList.add("copied"); setTimeout(() => { target.textContent = "Copy"; - target.style.backgroundColor = "var(--color-surface0)"; - target.style.color = "var(--color-text)"; + target.classList.remove("copied"); }, 2000); } catch (err) { console.error("Failed to copy code:", err); target.textContent = "Failed"; - target.style.backgroundColor = "var(--color-red)"; + target.classList.add("failed"); setTimeout(() => { target.textContent = "Copy"; - target.style.backgroundColor = "var(--color-surface0)"; + target.classList.remove("failed"); }, 2000); } } @@ -346,8 +370,6 @@ export default function PostBodyClient(props: PostBodyClientProps) { // Single event listener for all copy button interactions contentRef.addEventListener("click", handleCopyButtonInteraction); - contentRef.addEventListener("mouseover", handleCopyButtonInteraction); - contentRef.addEventListener("mouseout", handleCopyButtonInteraction); } }); @@ -357,7 +379,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { setTimeout(() => { processReferences(); if (props.hasCodeBlock) { - addCopyButtons(); + processCodeBlocks(); } }, 150); } diff --git a/src/routes/blog/post.css b/src/routes/blog/post.css index 13a0a30..54cb6f7 100644 --- a/src/routes/blog/post.css +++ b/src/routes/blog/post.css @@ -687,6 +687,106 @@ button:active, #post-content-body span#references-section-start { display: none !important; } +/* Code block styles for published posts */ +#post-content-body pre { + position: relative; + overflow: auto; + max-height: 60vh; + background-color: var(--color-crust); + color: #fff; + font-family: "Source Code Pro", monospace; + border-radius: 0.5rem; + + code { + overflow: visible; + max-height: none; + background: transparent; + color: inherit; + padding: 0; + font-size: 0.8rem; + display: block; + scrollbar-color: var(--color-text) transparent; + } +} + +/* Language header */ +#post-content-body .language-header { + padding: 0.5rem 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + background-color: var(--color-mantle); + color: var(--color-subtext1); + border-bottom: 1px solid var(--color-surface0); + border-top-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; + margin-bottom: 0; +} + +/* Pre with language header - remove top border radius */ +#post-content-body .language-header + pre { + border-top-left-radius: 0; + border-top-right-radius: 0; + margin-top: 0; +} + +/* Line numbers */ +#post-content-body .line-numbers { + position: absolute; + left: 0; + top: 0; + bottom: 0; + padding: 1rem 0.75rem; + text-align: right; + user-select: none; + pointer-events: none; + background-color: var(--color-crust); + border-right: 1px solid var(--color-surface0); + color: var(--color-overlay0); + font-size: 0.875rem; + line-height: 1.5rem; + font-family: inherit; + min-width: 3rem; +} + +/* Code with line numbers - add padding for line numbers column */ +#post-content-body pre:has(.line-numbers) code { + padding-left: 4rem; +} + +/* Copy button */ +#post-content-body .copy-button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 0.375rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + border-radius: 0.25rem; + transition: all 0.2s; + z-index: 10; + background-color: var(--color-surface0); + color: var(--color-text); + border: 1px solid var(--color-overlay0); + cursor: pointer; +} + +#post-content-body .copy-button:hover { + background-color: var(--color-surface1); +} + +#post-content-body .copy-button.copied { + background-color: var(--color-green); + color: var(--color-base); +} + +#post-content-body .copy-button.failed { + background-color: var(--color-red); + color: var(--color-base); +} + +/* Editor styles remain unchanged */ pre { background: #0d0d0d; color: #fff;