"use client"; import { useState, useEffect, useMemo, useCallback } from "react"; import { createHighlighter, type Highlighter } from "shiki"; type HtmlViewerProps = { filename: string; /** Raw URL for iframe rendering (served with text/html) */ rawUrl: string; /** JSON API URL to fetch source content on demand (for code view) */ contentUrl: string; }; type ViewMode = "rendered" | "code"; let highlighterPromise: Promise | null = null; function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = createHighlighter({ themes: ["github-dark", "github-light"], langs: ["html"], }); } return highlighterPromise; } export function HtmlViewer({ filename, rawUrl, contentUrl }: HtmlViewerProps) { const [mode, setMode] = useState("rendered"); const [source, setSource] = useState(null); const [sourceLoading, setSourceLoading] = useState(false); const handleCodeToggle = useCallback(() => { setMode("code"); if (source !== null) {return;} setSourceLoading(true); void fetch(contentUrl) .then((r) => r.json()) .then((data: { content: string }) => setSource(data.content)) .catch(() => setSource("")) .finally(() => setSourceLoading(false)); }, [contentUrl, source]); return (
{/* Header bar */}
{filename} HTML {/* Mode toggle */}
{/* Open in new tab */} { (e.currentTarget as HTMLElement).style.background = "var(--color-surface-hover)"; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = "transparent"; }} >
{/* Content */} {mode === "rendered" ? ( ) : sourceLoading || source === null ? (
) : ( )}
); } // --- Rendered HTML view (sandboxed iframe) --- function RenderedView({ rawUrl }: { rawUrl: string }) { return (