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) => `$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 `${items}
`; }); // 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 */}{/* Scrollable content */}How to Use Triple-C
{error && (Failed to load help content: {error}
)} {!markdown && !error && (Loading...
)} {markdown && ( )}