diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-03-01 11:49:55 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-03-01 11:49:55 +0900 |
| commit | 94f940cddc9ca39d484996616f9f4b322c1ff7f8 (patch) | |
| tree | 8694eb9984f9fb4a4f4c1bf312b7d202419e81c8 | |
| parent | 5b72323964c3fa7a10377b363714f681b5ffa709 (diff) | |
| download | phperkaigi-2026-albatross-94f940cddc9ca39d484996616f9f4b322c1ff7f8.tar.gz phperkaigi-2026-albatross-94f940cddc9ca39d484996616f9f4b322c1ff7f8.tar.zst phperkaigi-2026-albatross-94f940cddc9ca39d484996616f9f4b322c1ff7f8.zip | |
fix(frontend): render line numbers before syntax highlighting to prevent layout shift
Show plaintext with Shiki-compatible line structure (.shiki > code > .line)
from the start, so CSS line-number counters apply immediately. This prevents
the popover from jittering when Shiki replaces the content. If Shiki fails,
the plaintext with line numbers remains as a natural fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| -rw-r--r-- | frontend/app/components/Gaming/CodeBlock.tsx | 26 |
1 files changed, 22 insertions, 4 deletions
diff --git a/frontend/app/components/Gaming/CodeBlock.tsx b/frontend/app/components/Gaming/CodeBlock.tsx index 49ef01e..2107f94 100644 --- a/frontend/app/components/Gaming/CodeBlock.tsx +++ b/frontend/app/components/Gaming/CodeBlock.tsx @@ -8,12 +8,30 @@ type Props = { language: BundledLanguage; }; +function Plaintext({ code }: { code: string }) { + const lines = code.split("\n"); + return ( + <pre> + <code> + {lines.map((line, i) => ( + <span key={i} className="line"> + {line} + {i < lines.length - 1 ? "\n" : ""} + </span> + ))} + </code> + </pre> + ); +} + export default function CodeBlock({ code, language }: Props) { const [nodes, setNodes] = useState<JSX.Element | null>(null); const [showCopied, setShowCopied] = useState(false); useLayoutEffect(() => { - highlight(code, language).then(setNodes); + highlight(code, language) + .then(setNodes) + .catch(() => setNodes(null)); }, [code, language]); const handleCopy = () => { @@ -37,9 +55,9 @@ export default function CodeBlock({ code, language }: Props) { )} </button> )} - <pre className="h-full w-full p-2 pr-12 bg-white rounded-lg border border-gray-300 whitespace-pre-wrap break-words"> - {nodes === null ? <code>{code}</code> : nodes} - </pre> + <div className="shiki h-full w-full p-2 pr-12 bg-white rounded-lg border border-gray-300 whitespace-pre-wrap break-words"> + {nodes ?? <Plaintext code={code} />} + </div> </div> ); } |
