import { useEffect, useRef, useCallback, useState } from "react"; import { getHelpContent } from "../../lib/tauri-commands"; interface Props { onClose: () => void; } /** Convert header text to a URL-friendly slug for anchor links. */ function slugify(text: string): string { return text .toLowerCase() .replace(/<[^>]+>/g, "") // strip HTML tags (e.g. from inline code) .replace(/[^\w\s-]/g, "") // remove non-word chars except spaces/dashes .replace(/\s+/g, "-") // spaces to dashes .replace(/-+/g, "-") // collapse consecutive dashes .replace(/^-|-$/g, ""); // trim leading/trailing dashes } /** Simple markdown-to-HTML converter for the help content. */ function renderMarkdown(md: string): string { let html = md; // Normalize line endings html = html.replace(/\r\n/g, "\n"); // Escape HTML entities (but we'll re-introduce tags below) html = html.replace(/&/g, "&").replace(//g, ">"); // Fenced code blocks (```...```) html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) => { return `
${code.trimEnd()}
`; }); // Inline code (`...`) html = html.replace(/`([^`]+)`/g, '$1'); // Tables html = html.replace( /(?:^|\n)(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/g, (_m, headerRow: string, _sep: string, bodyRows: string) => { const headers = headerRow .split("|") .slice(1, -1) .map((c: string) => `${c.trim()}`) .join(""); const rows = bodyRows .trim() .split("\n") .map((row: string) => { const cells = row .split("|") .slice(1, -1) .map((c: string) => `${c.trim()}`) .join(""); return `${cells}`; }) .join(""); return `${headers}${rows}
`; }, ); // Blockquotes (> ...) html = html.replace(/(?:^|\n)> (.+)/g, '
$1
'); // Merge adjacent blockquotes html = html.replace(/<\/blockquote>\s*
/g, "
"); // Horizontal rules html = html.replace(/\n---\n/g, '
'); // Headers with id attributes for anchor navigation (process from h4 down to h1) html = html.replace(/^#### (.+)$/gm, (_m, title) => `

${title}

`); html = html.replace(/^### (.+)$/gm, (_m, title) => `

${title}

`); html = html.replace(/^## (.+)$/gm, (_m, title) => `

${title}

`); html = html.replace(/^# (.+)$/gm, (_m, title) => `

${title}

`); // Bold (**...**) html = html.replace(/\*\*([^*]+)\*\*/g, "$1"); // Italic (*...*) html = html.replace(/\*([^*]+)\*/g, "$1"); // Markdown-style anchor links [text](#anchor) html = html.replace( /\[([^\]]+)\]\(#([^)]+)\)/g, '$1', ); // Markdown-style external links [text](url) html = html.replace( /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '$1', ); // Unordered list items (- ...) // Group consecutive list items html = html.replace(/((?:^|\n)- .+(?:\n- .+)*)/g, (block) => { const items = block .trim() .split("\n") .map((line) => `
  • ${line.replace(/^- /, "")}
  • `) .join(""); return ``; }); // Ordered list items (1. ...) html = html.replace(/((?:^|\n)\d+\. .+(?:\n\d+\. .+)*)/g, (block) => { const items = block .trim() .split("\n") .map((line) => `
  • ${line.replace(/^\d+\. /, "")}
  • `) .join(""); return `
      ${items}
    `; }); // Links - convert bare URLs to clickable links (skip already-wrapped URLs) html = html.replace( /(?)(https?:\/\/[^\s<)]+)/g, '$1', ); // Wrap remaining loose text lines in paragraphs // Split by double newlines for paragraph breaks const blocks = html.split(/\n\n+/); html = blocks .map((block) => { const trimmed = block.trim(); if (!trimmed) return ""; // Don't wrap blocks that are already HTML elements if ( /^<(h[1-4]|ul|ol|pre|table|blockquote|hr)/.test(trimmed) ) { return trimmed; } // Wrap in paragraph, replacing single newlines with
    return `

    ${trimmed.replace(/\n/g, "
    ")}

    `; }) .join("\n"); return html; } export default function HelpDialog({ onClose }: Props) { const overlayRef = useRef(null); const contentRef = useRef(null); const [markdown, setMarkdown] = useState(null); const [error, setError] = useState(null); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [onClose]); useEffect(() => { getHelpContent() .then(setMarkdown) .catch((e) => setError(String(e))); }, []); const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === overlayRef.current) onClose(); }, [onClose], ); // Handle anchor link clicks to scroll within the dialog const handleContentClick = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement; const anchor = target.closest("a"); if (!anchor) return; const href = anchor.getAttribute("href"); if (!href || !href.startsWith("#")) return; e.preventDefault(); const el = contentRef.current?.querySelector(href); if (el) el.scrollIntoView({ behavior: "smooth" }); }, []); return (
    {/* Header */}

    How to Use Triple-C

    {/* Scrollable content */}
    {error && (

    Failed to load help content: {error}

    )} {!markdown && !error && (

    Loading...

    )} {markdown && (
    )}
    ); }