diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 62a5bee66..3e8694e65 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -8,7 +8,6 @@ import { renderMarkdown } from '@/lib/markdown'; import BlogReadingProgress from '@/components/BlogReadingProgress'; import BlogToc from '@/components/BlogToc'; import BlogShareBar from '@/components/BlogShareBar'; -import CodeBlockEnhancer from '@/components/CodeBlockEnhancer'; import NewsletterCTA from '@/components/NewsletterCTA'; import SponsorCallout from '@/components/SponsorCallout'; import AuthorSocials from '@/components/AuthorSocials'; @@ -301,9 +300,6 @@ export default async function BlogPost({ params }) { )} - - -
diff --git a/app/globals.css b/app/globals.css index 87344354c..9b1d0bb36 100644 --- a/app/globals.css +++ b/app/globals.css @@ -405,6 +405,25 @@ a:focus-visible, button:focus-visible { margin: 1.5em auto; } +.prose-blog .blog-video { + margin: 1.75em 0; +} +.prose-blog .blog-video video { + display: block; + width: 100%; + height: auto; + border-radius: 0.75rem; + border: 1px solid var(--border-subtle); + background: #05070a; +} +.prose-blog .blog-video figcaption { + margin-top: 0.65rem; + color: var(--text-secondary); + font-size: 0.875rem; + line-height: 1.5; + text-align: center; +} + .prose-blog hr { border: 0; height: 1px; @@ -487,6 +506,10 @@ a:focus-visible, button:focus-visible { border-radius: 0 0 0.75rem 0.75rem; margin-top: 0; } +.prose-blog [data-rehype-pretty-code-title] + .blog-code-block pre { + border-radius: 0 0 0.75rem 0.75rem; + margin-top: 0; +} /* embedded iframes (youtube, codepens) */ .prose-blog iframe { diff --git a/components/CodeBlock.jsx b/components/CodeBlock.jsx new file mode 100644 index 000000000..1288a4090 --- /dev/null +++ b/components/CodeBlock.jsx @@ -0,0 +1,43 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +export default function CodeBlock({ children, ...props }) { + const preRef = useRef(null); + const resetTimerRef = useRef(null); + const [copied, setCopied] = useState(false); + + useEffect(() => { + return () => { + if (resetTimerRef.current) clearTimeout(resetTimerRef.current); + }; + }, []); + + async function copyCode() { + const code = preRef.current?.innerText; + if (!code) return; + + try { + await navigator.clipboard.writeText(code); + setCopied(true); + if (resetTimerRef.current) clearTimeout(resetTimerRef.current); + resetTimerRef.current = setTimeout(() => setCopied(false), 1500); + } catch { + // Clipboard access can be blocked by browser or page permissions. + } + } + + return ( +
+
{children}
+ +
+ ); +} diff --git a/components/CodeBlockEnhancer.jsx b/components/CodeBlockEnhancer.jsx deleted file mode 100644 index aabd2a085..000000000 --- a/components/CodeBlockEnhancer.jsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; -import { useEffect } from 'react'; - -export default function CodeBlockEnhancer() { - useEffect(() => { - const blocks = document.querySelectorAll('article pre'); - blocks.forEach((pre) => { - if (pre.dataset.enhanced) return; - pre.dataset.enhanced = '1'; - - const wrapper = document.createElement('div'); - wrapper.className = 'relative group'; - pre.parentNode.insertBefore(wrapper, pre); - wrapper.appendChild(pre); - - const btn = document.createElement('button'); - btn.type = 'button'; - btn.setAttribute('aria-label', 'Copy code'); - btn.className = - 'absolute top-2 right-2 px-2 py-1 text-[11px] font-semibold rounded border border-white/20 bg-black/50 text-white/80 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-black/70'; - btn.textContent = 'Copy'; - btn.addEventListener('click', async () => { - const code = pre.querySelector('code')?.innerText || pre.innerText; - try { - await navigator.clipboard.writeText(code); - btn.textContent = 'Copied!'; - setTimeout(() => (btn.textContent = 'Copy'), 1500); - } catch {} - }); - wrapper.appendChild(btn); - }); - }, []); - - return null; -} diff --git a/components/DynamicMigLifecycleAnimation.jsx b/components/DynamicMigLifecycleAnimation.jsx new file mode 100644 index 000000000..c2581e601 --- /dev/null +++ b/components/DynamicMigLifecycleAnimation.jsx @@ -0,0 +1,314 @@ +const CURRENT_STEPS = [ + { label: 'Discover', detail: 'NVML reports profiles and legal placements' }, + { label: 'Reserve', detail: 'Scheduler selects the smallest fitting placement' }, + { label: 'Create', detail: 'Device plugin creates that exact GI and CI' }, + { label: 'Reclaim', detail: 'The pod ends and only its instance is removed' }, +]; + +export default function DynamicMigLifecycleAnimation() { + return ( +
+