Mercurial
diff hg-web/src/repo-browser.tsx @ 176:fed99fc04e12 hg-web
[HgWeb] Problem with the emscript lol
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Wed, 21 Jan 2026 19:32:08 -0800 |
| parents | 71ad34a8bc9a |
| children | 32ce881452fa |
line wrap: on
line diff
--- a/hg-web/src/repo-browser.tsx Tue Jan 20 06:06:47 2026 -0800 +++ b/hg-web/src/repo-browser.tsx Wed Jan 21 19:32:08 2026 -0800 @@ -1,24 +1,161 @@ import React, { useState, useEffect } from 'react'; +import { renderMarkdown } from './src/markdown_to_html.js'; + +// --- ICONS (Using CDN Links) --- +const ICONS = { + folder: "https://cdn-icons-png.flaticon.com/512/11471/11471391.png", + file: "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/document.svg", + home: "https://cdn-icons-png.flaticon.com/512/1946/1946488.png", + repo: "/public/epi_all_colors.svg", + clone: "https://cdn-icons-png.flaticon.com/512/11471/11471391.png" +}; const API_BASE = '/api/repo'; /** + * Component: Styles + * Injected CSS for the polished look + */ +const GlobalStyles = () => ( + <style>{` + :root { + --bg-color: #ffffff; + --bg-subtle: #f6f8fa; + --border-color: #d0d7de; + --accent-color: #0969da; + --text-primary: #1f2328; + --text-secondary: #656d76; + --hover-color: #f3f4f6; + --radius: 6px; + } + + .repo-container { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + max-width: 980px; + margin: 40px auto; + color: var(--text-primary); + padding: 0 20px; + } + + /* Header */ + .header { + display: flex; + align-items: center; + margin-bottom: 20px; + gap: 15px; + } + .header-icon { width: 32px; height: 32px; opacity: 0.8; } + .header h1 { margin: 0; font-size: 24px; font-weight: 600; } + .description { color: var(--text-secondary); margin: 0; font-size: 14px; } + + /* Clone Box */ + .clone-box { + background: var(--bg-subtle); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 12px 16px; + margin-bottom: 24px; + display: flex; + justify-content: space-between; + align-items: center; + } + .clone-label { font-weight: 600; font-size: 13px; margin-right: 10px; color: var(--text-primary); } + .clone-url { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + background: white; + border: 1px solid var(--border-color); + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + color: var(--text-secondary); + flex-grow: 1; + } + + /* Breadcrumb */ + #breadcrumb { + display: flex; + align-items: center; + font-size: 14px; + margin-bottom: 16px; + color: var(--text-secondary); + padding: 8px 0; + } + #breadcrumb a { + color: var(--accent-color); + text-decoration: none; + border-radius: 4px; + padding: 2px 6px; + } + #breadcrumb a:hover { background: var(--bg-subtle); text-decoration: underline; } + #breadcrumb .separator { margin: 0 4px; color: var(--text-secondary); opacity: 0.5; } + #breadcrumb .nav-item.active { font-weight: 600; color: var(--text-primary); padding: 2px 6px;} + + /* File List Table Structure */ + .file-list-container { + border: 1px solid var(--border-color); + border-radius: var(--radius); + overflow: hidden; + } + .file-header { + background: var(--bg-subtle); + border-bottom: 1px solid var(--border-color); + padding: 12px 16px; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + } + .empty-state { padding: 40px; text-align: center; color: var(--text-secondary); } + .error-message { + padding: 15px; border: 1px solid #ffdce0; + background: #ffebe9; color: #cf222e; + border-radius: var(--radius); margin-bottom: 20px; + } + + /* File Row */ + .file-row { + display: flex; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid var(--border-color); + transition: background 0.1s; + } + .file-row:last-child { border-bottom: none; } + .file-row:hover { background: var(--hover-color); } + + .file-row .icon img { width: 20px; height: 20px; vertical-align: middle; margin-right: 12px; } + .file-row .name a { + color: var(--text-primary); + text-decoration: none; + font-size: 14px; + } + .file-row .name a:hover { color: var(--accent-color); text-decoration: underline; } + + /* Readme */ + #readmeSection { margin-top: 32px; border: 1px solid var(--border-color); border-radius: var(--radius); } + .readme-header { + background: var(--bg-subtle); + padding: 10px 16px; + font-size: 12px; font-weight: 600; + border-bottom: 1px solid var(--border-color); + display: flex; align-items: center; gap: 8px; + } + #readmeContent { padding: 32px; background: white; overflow-x: auto; } + `}</style> +); + +/** * Component: Breadcrumb - * Renders the navigation path at the top */ function Breadcrumb({ currentPath, onNavigate }) { if (!currentPath) { return ( <nav id="breadcrumb"> - <span className="nav-item active">Root</span> + <span className="nav-item active">root</span> </nav> ); } const parts = currentPath.split('/').filter(p => p); - - // Create cumulative paths for links - // e.g., src/components -> ['src', 'src/components'] const crumbs = parts.map((part, index) => ({ name: part, fullPath: parts.slice(0, index + 1).join('/') @@ -29,14 +166,15 @@ <a href="/" onClick={(e) => { e.preventDefault(); onNavigate(''); }} + title="Go to Root" > - Root + root </a> {crumbs.map((crumb, index) => { const isLast = index === crumbs.length - 1; return ( <React.Fragment key={crumb.fullPath}> - <span className="separator"> / </span> + <span className="separator">/</span> {isLast ? ( <span className="nav-item active">{crumb.name}</span> ) : ( @@ -56,55 +194,60 @@ /** * Component: FileList - * Renders the table of directories and files */ function FileList({ directories, files, onNavigate }) { const isEmpty = directories.length === 0 && files.length === 0; if (isEmpty) { - return <div className="empty-state">No files found.</div>; + return ( + <div className="file-list-container"> + <div className="empty-state">This directory is empty.</div> + </div> + ); } return ( - <div id="fileList"> - {/* Render Directories */} - {directories.map((dir) => ( - <FileRow - key={dir.abspath} - item={dir} - icon="📁" - isDir={true} - onNavigate={onNavigate} - /> - ))} + <div className="file-list-container"> + {/* Optional header row like GitHub */} + <div className="file-header"> + Files + </div> - {/* Render Files */} - {files.map((file) => ( - <FileRow - key={file.abspath} - item={file} - icon="📄" - isDir={false} - /> - ))} + <div id="fileListBody"> + {directories.map((dir) => ( + <FileRow + key={dir.abspath} + item={dir} + iconUrl={ICONS.folder} + isDir={true} + onNavigate={onNavigate} + /> + ))} + + {files.map((file) => ( + <FileRow + key={file.abspath} + item={file} + iconUrl={ICONS.file} + isDir={false} + /> + ))} + </div> </div> ); } /** * Component: FileRow - * Individual item row */ -function FileRow({ item, icon, isDir, onNavigate }) { +function FileRow({ item, iconUrl, isDir, onNavigate }) { const handleClick = (e) => { if (isDir) { e.preventDefault(); onNavigate(item.abspath); } - // Files let the default <a> behavior happen (download/open in new tab) }; - // Files link to the raw content API, Dirs link to the app view const href = isDir ? `?path=${encodeURIComponent(item.abspath)}` : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`; @@ -112,8 +255,10 @@ const target = isDir ? undefined : "_blank"; return ( - <div className={`file-item ${item.type}`}> - <span className="icon">{icon}</span> + <div className="file-row"> + <span className="icon"> + <img src={iconUrl} alt={isDir ? "Directory" : "File"} /> + </span> <span className="name"> <a href={href} onClick={handleClick} target={target} rel="noreferrer"> {item.basename} @@ -125,56 +270,49 @@ /** * Component: ReadmeViewer - * Renders the README content */ function ReadmeViewer({ content }) { if (!content) return null; + useEffect(() => renderMarkdown(content, readmeContent), [content]); + return ( - <div id="readmeSection" style={{ marginTop: '20px', borderTop: '1px solid #eee' }}> - <h3>README.md</h3> - <div id="readmeContent"> - <pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}> - {content} - </pre> + <div id="readmeSection"> + <div className="readme-header"> + <img src="https://img.icons8.com/material-outlined/24/000000/menu--v1.png" width="16" alt="" style={{opacity:0.5}} /> + README.md </div> + <div id="readmeContent"></div> </div> ); } - - /** * Main Application Component */ function RepoBrowser() { - // State management for path, data, and UI states const [currentPath, setCurrentPath] = useState(getCurrentPath()); const [content, setContent] = useState({ files: [], directories: [] }); const [readme, setReadme] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - // Helper to get path from URL query params function getCurrentPath() { const params = new URLSearchParams(window.location.search); return params.get('path') || ''; } - // Effect: Handle Browser Navigation (Back/Forward buttons) useEffect(() => { const handlePopState = () => setCurrentPath(getCurrentPath()); window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); - // Effect: Fetch Data whenever currentPath changes useEffect(() => { fetchDirectory(currentPath); fetchReadme(currentPath); }, [currentPath]); - // Internal navigation handler (avoids full page reload) const navigate = (path) => { const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/'; window.history.pushState({ path }, '', newUrl); @@ -194,7 +332,6 @@ if (data.error) throw new Error(data.error); - // Ensure we always have arrays even if API returns null setContent({ files: data.files || [], directories: data.directories || [] @@ -208,7 +345,7 @@ }; const fetchReadme = async (path) => { - setReadme(null); // Reset previous readme + setReadme(null); try { const readmePath = path ? `${path}/README.md` : 'README.md'; const response = await fetch(`${API_BASE}/readme?path=${encodeURIComponent(readmePath)}`); @@ -217,40 +354,52 @@ const text = await response.text(); setReadme(text); } - } catch (err) { - // Silently fail for Readme as it's optional - } + } catch (err) { /* Silently fail */ } }; return ( - <div className="repo-container"> - <div class="header"> - <h1>Zenbu Repository</h1> - <p class="description">Browse and clone this mercurial repository</p> - </div> + <> + <GlobalStyles /> + <div className="repo-container"> + + {/* Header */} + <div className="header"> + <img src={ICONS.repo} alt="Repo" className="header-icon" /> + <div> + <h1>Zenbu Repository</h1> + <p className="description">Browse and manage the mercurial codebase</p> + </div> + </div> - <div class="clone-info"> - <strong>Clone this repository:</strong> - <p><code>hg clone http://zenbu.babocoder.com/repo</code></p> - </div> + {/* Clone Bar */} + <div className="clone-box"> + <div style={{display:'flex', alignItems:'center', width:'100%'}}> + <span className="clone-label">Clone HTTPS</span> + <code className="clone-url">hg clone http://zenbu.babocoder.com/repo</code> + </div> + </div> - <Breadcrumb currentPath={currentPath} onNavigate={navigate} /> - - {error && <div className="error-message">Error: {error}</div>} - - {loading ? ( - <div className="loading">Loading...</div> - ) : ( - <> - <FileList - directories={content.directories} - files={content.files} - onNavigate={navigate} - /> - <ReadmeViewer content={readme} /> - </> - )} - </div> + {/* Navigation & Content */} + <Breadcrumb currentPath={currentPath} onNavigate={navigate} /> + + {error && <div className="error-message">Error: {error}</div>} + + {loading ? ( + <div className="file-list-container" style={{padding: '40px', textAlign: 'center', color:'#666'}}> + Loading files... + </div> + ) : ( + <> + <FileList + directories={content.directories} + files={content.files} + onNavigate={navigate} + /> + <ReadmeViewer content={readme} /> + </> + )} + </div> + </> ); }