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>
+    </>
   );
 }