aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-03-01 11:49:55 +0900
committernsfisis <nsfisis@gmail.com>2026-03-01 11:49:55 +0900
commit94f940cddc9ca39d484996616f9f4b322c1ff7f8 (patch)
tree8694eb9984f9fb4a4f4c1bf312b7d202419e81c8 /frontend/app
parent5b72323964c3fa7a10377b363714f681b5ffa709 (diff)
downloadphperkaigi-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>
Diffstat (limited to 'frontend/app')
-rw-r--r--frontend/app/components/Gaming/CodeBlock.tsx26
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>
);
}