changeset 193:9f4429c49733 hg-web

[HgWeb] Making progress....
author MrJuneJune <me@mrjunejune.com>
date Sun, 25 Jan 2026 20:04:55 -0800
parents b818a4561a3c
children fb28063dc490
files gui_ze/gui_ze.bzl hg-web/BUILD hg-web/main.c hg-web/src/base.css hg-web/src/build.ts hg-web/src/components/app.tsx hg-web/src/components/directory-browser.tsx hg-web/src/components/footer.tsx hg-web/src/components/graph.tsx hg-web/src/components/header.tsx hg-web/src/components/repo-browser.tsx hg-web/src/components/theme.tsx hg-web/src/index.css hg-web/src/index.html hg-web/src/main.tsx hg-web/src/pencil_texture.png hg-web/src/repo-browser.tsx mrjunejune/BUILD react_games/public/base.css
diffstat 19 files changed, 2923 insertions(+), 1100 deletions(-) [+]
line wrap: on
line diff
--- a/gui_ze/gui_ze.bzl	Sat Jan 24 21:52:14 2026 -0800
+++ b/gui_ze/gui_ze.bzl	Sun Jan 25 20:04:55 2026 -0800
@@ -132,7 +132,7 @@
 {copy_commands}
 export NODE_PATH=./third_party/bun/node_modules
 cp ./third_party/bun/tsconfig.json .
-{bun} build {entry} --outfile {output} --target browser
+{bun} build {entry} --outfile {output} --target browser 
 """.format(
       copy_commands = "\n".join(copy_commands),
       src_package = src_package,
--- a/hg-web/BUILD	Sat Jan 24 21:52:14 2026 -0800
+++ b/hg-web/BUILD	Sun Jan 25 20:04:55 2026 -0800
@@ -41,9 +41,15 @@
   dest = "src/public",
 )
 
+move_files_into_dir(
+  name = "public_fonts_files",
+  srcs = ["//mrjunejune:public_fonts_files"],
+  dest = "src/public/fonts",
+)
+
 filegroup(
   name = "all_assets",
-  srcs = glob(["src/**"]) + [":compiled_js", ":public_files"],
+  srcs = glob(["src/**"]) + [":compiled_js", ":public_files", ":public_fonts_files"],
 )
 
 # Server binaries
--- a/hg-web/main.c	Sat Jan 24 21:52:14 2026 -0800
+++ b/hg-web/main.c	Sun Jan 25 20:04:55 2026 -0800
@@ -127,6 +127,71 @@
   return resp;
 }
 
+Seobeo_Request_Entry* ApiGetGraph(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+
+  void *path_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
+  const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
+  Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: rel_path='%s'\n", rel_path);
+  void *graph_id_kv = Dowa_HashMap_Get_Ptr(req, ":graph_id");
+  char *graph_id = ((Seobeo_Request_Entry*)graph_id_kv)->value;
+  Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: graph_id='%s'\n", graph_id);
+  char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
+  Seobeo_Url_Decode(decoded_path, rel_path);
+  char *safe_path = sanitize_path(decoded_path, arena);
+
+  Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: safe_path='%s'\n", safe_path);
+
+  if (strlen(safe_path) == 0)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena);
+    return resp;
+  }
+
+  char hg_path[MAX_PATH];
+  // void *graph_id_kv = Dowa_HashMap_Get_Ptr(req, ":graph_id");
+  // char *graph_id = ((Seobeo_Request_Entry*)graph_id_kv)->value;
+  snprintf(hg_path, sizeof(hg_path), "/graph/%s?%s", graph_id, safe_path);
+  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL);
+
+  Seobeo_Log(SEOBEO_DEBUG, "ApiGetGraph: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
+
+  char status[4];
+  snprintf(status, 4, "%i", hg_response->status_code);
+
+  if (!hg_response->body)
+  {
+    Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena);
+    return resp;
+  }
+
+  if (hg_response->status_code != 200)
+  {
+    Seobeo_Log(SEOBEO_DEBUG, "ApiGetGraph: error hg_response: %s\n", hg_response->body);
+    Dowa_HashMap_Push_Arena(resp, "status", status, arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
+    return resp;
+  }
+
+
+  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
+  char *temp2 = Dowa_Arena_Allocate(arena, 256);
+  snprintf(temp2, 256, "%zu", hg_response->body_length);
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
+
+  return resp;
+}
+
 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
@@ -345,11 +410,30 @@
   return resp;
 }
 
+Seobeo_Request_Entry* GetReactHome(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  size_t file_size = 0;
+  char *html = Seobeo_Web_LoadFile("/index.html", &file_size);
+
+  printf("%s", html);
+  Seobeo_Request_Entry *resp = NULL; 
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "text/html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", html, arena);
+  return resp;
+}
+
 int main(void) {
   Seobeo_Router_Init();
 
+
+  Seobeo_Router_Register("GET", "/", GetReactHome);
+  Seobeo_Router_Register("GET", "/directories", GetReactHome);
+  Seobeo_Router_Register("GET", "/graph", GetReactHome);
+
   Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory);
   Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile);
+  Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph);
   Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme);
 
   // Use streaming handler for hg wire protocol... 
--- a/hg-web/src/base.css	Sat Jan 24 21:52:14 2026 -0800
+++ b/hg-web/src/base.css	Sun Jan 25 20:04:55 2026 -0800
@@ -1,123 +1,169 @@
-/* --- Colors  ---*/
+/* Reset CSS: https://meyerweb.com/eric/tools/css/reset/ */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+
+/* ===========================================
+   Base CSS - Color Variables and Basic Setup
+   =========================================== */
+
+/* Light mode (default) */
 :root {
   --bg: #ffffff;
-  --fg: #1a1a1a;
-  --border: #e0e0e0;
-  --hover: #f5f5f5;
-  --accent: #0066cc;
-  --accent-hover: #0052a3;
-  --secondary: #6c757d;
-  --success: #28a745;
-  --warning: #ffc107;
-  --danger: #dc3545;
-  --code-bg: #f6f8fa;
-  --link: #0066cc;
-  --link-hover: #0052a3;
+  --bg-subtle: #f6f8fa;
+  --bg-code: #f6f8fa;
+  --border: #d0d7de;
+  --accent: #0969da;
+  --text-primary: #1f2328;
+  --text-secondary: #656d76;
+  --hover: #f3f4f6;
+  --success: #1a7f37;
+  --danger: #cf222e;
+  --danger-bg: #ffebe9;
+  --danger-border: #ffdce0;
+  --overlay: rgba(0, 0, 0, 0.5);
+
+  /* Graph colors - light mode */
+  --graph-1: #495057;
+  --graph-2: #1971c2;
+  --graph-3: #099268;
+  --graph-4: #e67700;
+  --graph-5: #7048e8;
+  --graph-6: #c92a2a;
+  --graph-7: #c2255c;
+  --graph-node-border: #ffffff;
 }
 
-.dark {
+/* Dark mode - applied when html has .dark class */
+:root.dark {
   --bg: #0d1117;
-  --fg: #c9d1d9;
+  --bg-subtle: #161b22;
+  --bg-code: #161b22;
   --border: #30363d;
-  --hover: #161b22;
   --accent: #58a6ff;
-  --accent-hover: #79c0ff;
-  --secondary: #8b949e;
-  --success: #3fb950;
-  --warning: #d29922;
+  --text-primary: #e6edf3;
+  --text-secondary: #8b949e;
+  --hover: #1c2128;
+  --success: #238636;
   --danger: #f85149;
-  --code-bg: #161b22;
-  --link: #58a6ff;
-  --link-hover: #79c0ff;
+  --danger-bg: #f8514926;
+  --danger-border: #f8514966;
+  --overlay: rgba(0, 0, 0, 0.7);
+
+  /* Graph colors - dark mode */
+  --graph-1: #868e96;
+  --graph-2: #4dabf7;
+  --graph-3: #63e6be;
+  --graph-4: #ffbc42;
+  --graph-5: #b197fc;
+  --graph-6: #ff8787;
+  --graph-7: #f06595;
+  --graph-node-border: #1a1a1a;
 }
 
+/* System preference fallback (when no explicit class is set) */
 @media (prefers-color-scheme: dark) {
-  :root:not(.light-mode) {
+  :root:not(.light) {
     --bg: #0d1117;
-    --fg: #c9d1d9;
+    --bg-subtle: #161b22;
+    --bg-code: #161b22;
     --border: #30363d;
-    --hover: #161b22;
     --accent: #58a6ff;
-    --accent-hover: #79c0ff;
-    --secondary: #8b949e;
-    --success: #3fb950;
-    --warning: #d29922;
+    --text-primary: #e6edf3;
+    --text-secondary: #8b949e;
+    --hover: #1c2128;
+    --success: #238636;
     --danger: #f85149;
-    --code-bg: #161b22;
-    --link: #58a6ff;
-    --link-hover: #79c0ff;
+    --danger-bg: #f8514926;
+    --danger-border: #f8514966;
+    --overlay: rgba(0, 0, 0, 0.7);
+
+    --graph-1: #868e96;
+    --graph-2: #4dabf7;
+    --graph-3: #63e6be;
+    --graph-4: #ffbc42;
+    --graph-5: #b197fc;
+    --graph-6: #ff8787;
+    --graph-7: #f06595;
+    --graph-node-border: #1a1a1a;
   }
 }
 
-/* --- Reset and Base Styles --- */
+/* Fonts */
+@font-face {
+  font-family: "Roboto";
+  src: url("/public/fonts/Roboto-Regular.ttf");
+}
+@font-face {
+  font-family: "Roboto Light";
+  src: url("/public/fonts/Roboto-Thin.ttf");
+}
+@font-face {
+  font-family: "More Thin";
+  src: url("/public/fonts/more-sugar.thin.otf");
+}
+@font-face {
+  font-family: "More";
+  src: url("/public/fonts/more-sugar.regular.otf");
+}
+
+button {
+  font-family: "More Thin", sans-serif;
+}
+
+/* Reset and Base */
 * {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
 }
 
-html {
-  background: var(--bg);
-  color: var(--fg);
-}
-
-body {
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
-  line-height: 1.6;
+html, body {
   background: var(--bg);
-  color: var(--fg);
-  font-size: 16px;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-main {
-  max-width: 1200px;
-  margin: 0 auto;
-  padding: 2rem;
+  color: var(--text-primary);
+  font-family: "More Thin", sans-serif;
+  line-height: 1.6;
+  transition: background 0.2s, color 0.2s;
 }
 
 a {
-  color: var(--link);
+  color: var(--accent);
   text-decoration: none;
 }
 
 a:hover {
-  color: var(--link-hover);
   text-decoration: underline;
 }
 
-h1, h2, h3, h4, h5, h6 {
-  margin-bottom: 1rem;
-  font-weight: 600;
-  line-height: 1.25;
-}
-
-h1 { font-size: 2rem; }
-h2 { font-size: 1.75rem; }
-h3 { font-size: 1.5rem; }
-h4 { font-size: 1.25rem; }
-h5 { font-size: 1.1rem; }
-h6 { font-size: 1rem; }
-
-p {
-  margin-bottom: 1rem;
-}
-
 code {
-  background: var(--code-bg);
+  background: var(--bg-code);
   padding: 0.2em 0.4em;
   border-radius: 3px;
-  font-family: 'Monaco', 'Courier New', monospace;
   font-size: 0.9em;
 }
 
 pre {
-  background: var(--code-bg);
+  background: var(--bg-code);
   padding: 1rem;
   border-radius: 6px;
   overflow-x: auto;
-  margin-bottom: 1rem;
 }
 
 pre code {
@@ -125,17 +171,18 @@
   padding: 0;
 }
 
-/* Mobile responsive */
-@media (max-width: 768px) {
-  body {
-    font-size: 14px;
-  }
+/* Icon invert for dark mode */
+:root.dark .icon-invert {
+  filter: invert(0.8);
+}
 
-  main {
-    padding: 1rem;
-  }
+:root.light .icon-invert,
+:root:not(.dark):not(.light) .icon-invert {
+  filter: none;
+}
 
-  h1 { font-size: 1.75rem; }
-  h2 { font-size: 1.5rem; }
-  h3 { font-size: 1.25rem; }
+@media (prefers-color-scheme: dark) {
+  :root:not(.light) .icon-invert {
+    filter: invert(0.8);
+  }
 }
--- a/hg-web/src/build.ts	Sat Jan 24 21:52:14 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-import { readdir } from "node:fs/promises";
-const files = await readdir(import.meta.dir);
-console.log(files);
-
-
-const outputPath = Bun.argv[2];
-
-if (!outputPath) {
-  console.error("Please provide an output path. Usage: bun build.ts <output_path>");
-  process.exit(1);
-}
-
-const build = await Bun.build({
-  entrypoints: ["./hg-web/src/main.tsx"], 
-  outdir: outputPath,
-  metafile: true, 
-});
-
-if (build.success) {
-  console.log(`Build successful! Files saved to: ${outputPath}`);
-  console.log(JSON.stringify(build.metafile, null, 2));
-} else {
-  console.error("Build failed:", build.logs);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/app.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,389 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Graph, useGraphData } from "hg-web/src/components/graph";
+import { DirectoryBrowser } from "hg-web/src/components/directory-browser";
+import { Header } from "hg-web/src/components/header";
+import { Footer } from "hg-web/src/components/footer";
+import { ThemeProvider, useTheme } from "hg-web/src/components/theme";
+
+type Page = 'landing' | 'graph' | 'directory';
+
+type RouteState = {
+  page: Page;
+  graphCommit?: string;
+  graphTip?: string;
+  dirPath?: string;
+}
+
+// Icons
+const ICONS = {
+  folder: "/icons/folder.png",
+};
+
+const GraphIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+    <circle cx="6" cy="6" r="3"/>
+    <circle cx="6" cy="18" r="3"/>
+    <circle cx="18" cy="12" r="3"/>
+    <line x1="6" y1="9" x2="6" y2="15"/>
+    <path d="M8.5 7.5L15.5 11"/>
+  </svg>
+);
+
+const FolderIcon = () => (
+  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
+    <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
+  </svg>
+);
+
+const API_BASE = '/api/repo';
+
+function parseRoute(): RouteState {
+  const params = new URLSearchParams(window.location.search);
+  const pathname = window.location.pathname;
+
+  if (pathname.startsWith('/graph') || params.has('graph')) {
+    return {
+      page: 'graph',
+      graphCommit: params.get('commit') || undefined,
+      graphTip: params.get('tip') || undefined,
+    };
+  }
+
+  if (pathname.startsWith('/directory') || params.has('path')) {
+    return {
+      page: 'directory',
+      dirPath: params.get('path') || '',
+    };
+  }
+
+  return { page: 'landing' };
+}
+
+function buildUrl(state: RouteState): string {
+  const params = new URLSearchParams();
+
+  switch (state.page) {
+    case 'graph':
+      if (state.graphCommit) params.set('commit', state.graphCommit);
+      if (state.graphTip) params.set('tip', state.graphTip);
+      return `/graph${params.toString() ? '?' + params.toString() : ''}`;
+    case 'directory':
+      if (state.dirPath) params.set('path', state.dirPath);
+      return `/directory${params.toString() ? '?' + params.toString() : ''}`;
+    default:
+      return '/';
+  }
+}
+
+// Landing Page Component
+function LandingPage({
+  onNavigateToGraph,
+  onNavigateToDirectory,
+}: {
+  onNavigateToGraph: () => void;
+  onNavigateToDirectory: (path?: string) => void;
+}) {
+  const [directories, setDirectories] = useState<any[]>([]);
+  const [files, setFiles] = useState<any[]>([]);
+  const [dirLoading, setDirLoading] = useState(true);
+
+  const { data: graphData, loading: graphLoading } = useGraphData();
+
+  useEffect(() => {
+    fetch(`${API_BASE}/list`)
+      .then(r => r.json())
+      .then(data => {
+        setDirectories(data.directories || []);
+        setFiles(data.files || []);
+        setDirLoading(false);
+      })
+      .catch(() => setDirLoading(false));
+  }, []);
+
+  const previewItems = [
+    ...directories.slice(0, 6),
+    ...files.slice(0, Math.max(0, 6 - directories.length))
+  ].slice(0, 6);
+
+  return (
+    <div className="landing-grid">
+      {/* Graph Preview */}
+      <div className="landing-section">
+        <div className="landing-section-header">
+          <span className="landing-section-title">
+            <GraphIcon />
+            Recent Commits
+          </span>
+          <a href="/graph" className="landing-section-link" onClick={(e) => {
+            e.preventDefault();
+            onNavigateToGraph();
+          }}>
+            View all
+          </a>
+        </div>
+        <div className="landing-section-content">
+          {graphLoading ? (
+            <div className="loading-state">Loading commits...</div>
+          ) : graphData ? (
+            <Graph
+              data={graphData}
+              maxRows={8}
+              onCommitClick={(node) => {
+                console.log('Clicked commit:', node);
+              }}
+            />
+          ) : (
+            <div className="empty-state">Failed to load commits</div>
+          )}
+        </div>
+      </div>
+
+      {/* Directory Preview */}
+      <div className="landing-section">
+        <div className="landing-section-header">
+          <span className="landing-section-title">
+            <FolderIcon />
+            Repository Files
+          </span>
+          <a href="/directory" className="landing-section-link" onClick={(e) => {
+            e.preventDefault();
+            onNavigateToDirectory();
+          }}>
+            Browse all
+          </a>
+        </div>
+        <div className="landing-section-content">
+          {dirLoading ? (
+            <div className="loading-state">Loading files...</div>
+          ) : previewItems.length > 0 ? (
+            previewItems.map((item) => (
+              <div
+                key={item.abspath}
+                className="dir-item"
+                onClick={() => onNavigateToDirectory(item.abspath)}
+              >
+                <span className="dir-item-icon">
+                  <img
+                    className="icon-invert"
+                    src={directories.includes(item) ? ICONS.folder : "/icons/file.svg"}
+                    alt=""
+                  />
+                </span>
+                <span className="dir-item-name">{item.basename}</span>
+              </div>
+            ))
+          ) : (
+            <div className="empty-state">No files found</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// Graph Page Component
+function GraphPage({
+  onBack,
+  initialCommit,
+  initialTip,
+}: {
+  onBack: () => void;
+  initialCommit?: string;
+  initialTip?: string;
+}) {
+  const { data, loading, error, loadMore, hasMore, tip, currentCommit } = useGraphData({
+    initialCommit: initialCommit || null,
+    graphTop: initialTip || null,
+  });
+
+  useEffect(() => {
+    if (tip && currentCommit) {
+      const params = new URLSearchParams();
+      params.set('commit', currentCommit);
+      params.set('tip', tip);
+      const newUrl = `/graph?${params.toString()}`;
+      window.history.replaceState({ page: 'graph', graphCommit: currentCommit, graphTip: tip }, '', newUrl);
+    }
+  }, [currentCommit, tip]);
+
+  return (
+    <div>
+      <div className="page-header">
+        <button className="back-button" onClick={onBack}>
+          &larr; Back
+        </button>
+        <span className="page-title">Commit Graph</span>
+      </div>
+
+      {tip && (
+        <div className="graph-params">
+          <span className="graph-param">
+            <span className="graph-param-label">Tip:</span>
+            <span className="graph-param-value">{tip.substring(0, 12)}</span>
+          </span>
+          {currentCommit && currentCommit !== tip && (
+            <span className="graph-param">
+              <span className="graph-param-label">Current:</span>
+              <span className="graph-param-value">{currentCommit.substring(0, 12)}</span>
+            </span>
+          )}
+        </div>
+      )}
+
+      {error && (
+        <div className="error-message">Error: {error}</div>
+      )}
+
+      <Graph
+        data={data}
+        loading={loading}
+        hasMore={hasMore}
+        onLoadMore={loadMore}
+        onCommitClick={(node) => {
+          console.log('Clicked commit:', node);
+        }}
+      />
+    </div>
+  );
+}
+
+// Directory Page Component
+function DirectoryPage({
+  onBack,
+  initialPath,
+  onPathChange,
+}: {
+  onBack: () => void;
+  initialPath?: string;
+  onPathChange: (path: string) => void;
+}) {
+  return (
+    <div>
+      <div className="page-header">
+        <button className="back-button" onClick={onBack}>
+          &larr; Back
+        </button>
+        <span className="page-title">Repository Files</span>
+      </div>
+
+      <DirectoryBrowser
+        initialPath={initialPath}
+        onPathChange={onPathChange}
+      />
+    </div>
+  );
+}
+
+// Main App Content (uses theme context)
+function AppContent() {
+  const [route, setRoute] = useState<RouteState>(parseRoute);
+  const { isDark, toggleTheme } = useTheme();
+
+  // Handle browser back/forward
+  useEffect(() => {
+    const handlePopState = () => {
+      setRoute(parseRoute());
+    };
+    window.addEventListener('popstate', handlePopState);
+    return () => window.removeEventListener('popstate', handlePopState);
+  }, []);
+
+  const navigate = useCallback((newRoute: RouteState) => {
+    const url = buildUrl(newRoute);
+    window.history.pushState(newRoute, '', url);
+    setRoute(newRoute);
+  }, []);
+
+  const navigateToLanding = useCallback(() => {
+    navigate({ page: 'landing' });
+  }, [navigate]);
+
+  const navigateToGraph = useCallback((commit?: string, tip?: string) => {
+    navigate({ page: 'graph', graphCommit: commit, graphTip: tip });
+  }, [navigate]);
+
+  const navigateToDirectory = useCallback((path?: string) => {
+    navigate({ page: 'directory', dirPath: path || '' });
+  }, [navigate]);
+
+  const handleDirectoryPathChange = useCallback((path: string) => {
+    // Update URL without full navigation
+    const params = new URLSearchParams();
+    if (path) params.set('path', path);
+    const newUrl = `/directory${params.toString() ? '?' + params.toString() : ''}`;
+    window.history.replaceState({ page: 'directory', dirPath: path }, '', newUrl);
+    setRoute(prev => ({ ...prev, dirPath: path }));
+  }, []);
+
+  return (
+    <div className="app-container">
+      <Header
+        title="Zenbu Repository"
+        showThemeToggle={true}
+        isDark={isDark}
+        onToggleTheme={toggleTheme}
+      />
+
+      {/* Navigation Tabs */}
+      <div className="nav-tabs">
+        <button
+          className={`nav-tab ${route.page === 'landing' ? 'active' : ''}`}
+          onClick={navigateToLanding}
+        >
+          Home
+        </button>
+        <button
+          className={`nav-tab ${route.page === 'graph' ? 'active' : ''}`}
+          onClick={() => navigateToGraph()}
+        >
+          <GraphIcon />
+          Graph
+        </button>
+        <button
+          className={`nav-tab ${route.page === 'directory' ? 'active' : ''}`}
+          onClick={() => navigateToDirectory()}
+        >
+          <FolderIcon />
+          Files
+        </button>
+      </div>
+
+      {/* Page Content */}
+      {route.page === 'landing' && (
+        <LandingPage
+          onNavigateToGraph={() => navigateToGraph()}
+          onNavigateToDirectory={navigateToDirectory}
+        />
+      )}
+
+      {route.page === 'graph' && (
+        <GraphPage
+          onBack={navigateToLanding}
+          initialCommit={route.graphCommit}
+          initialTip={route.graphTip}
+        />
+      )}
+
+      {route.page === 'directory' && (
+        <DirectoryPage
+          onBack={navigateToLanding}
+          initialPath={route.dirPath}
+          onPathChange={handleDirectoryPathChange}
+        />
+      )}
+
+      <Footer />
+    </div>
+  );
+}
+
+// App wrapper with ThemeProvider
+function App() {
+  return (
+    <ThemeProvider>
+      <AppContent />
+    </ThemeProvider>
+  );
+}
+
+export { App };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/directory-browser.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,539 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js';
+import hljs from 'third_party/highlight/highlight.min.js';
+
+// --- ICONS (served as static files) ---
+const ICONS = {
+  folder: "/icons/folder.png",
+  file: "/icons/file.svg",
+  close: "/icons/close.png"
+};
+
+const API_BASE = '/api/repo';
+
+// File extensions that should be displayed as code
+const CODE_EXTENSIONS = new Set([
+  'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp',
+  'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash',
+  'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss',
+  'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg',
+  'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
+  'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
+  'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample'
+]);
+
+// Prefetch cache
+const prefetchCache = new Map<string, Promise<any>>();
+
+function isCodeFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  const basename = filename.toLowerCase();
+  return CODE_EXTENSIONS.has(ext) ||
+         CODE_EXTENSIONS.has(basename) ||
+         basename === 'dockerfile' ||
+         basename === 'makefile' ||
+         basename.startsWith('.');
+}
+
+function isMarkdownFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  return ext === 'md' || ext === 'markdown';
+}
+
+function prefetchDirectory(path: string): void {
+  const cacheKey = `dir:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  const url = path
+    ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+    : `${API_BASE}/list`;
+
+  prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null));
+}
+
+function prefetchFile(path: string): void {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  prefetchCache.set(cacheKey,
+    fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`)
+      .then(r => r.ok ? r.text() : null)
+      .catch(() => null)
+  );
+}
+
+async function getCachedFile(path: string): Promise<string | null> {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) {
+    return prefetchCache.get(cacheKey);
+  }
+  prefetchFile(path);
+  return prefetchCache.get(cacheKey)!;
+}
+
+/**
+ * Component: Breadcrumb
+ */
+function Breadcrumb({ currentPath, onNavigate }: { currentPath: string; onNavigate: (path: string) => void }) {
+  if (!currentPath) {
+    return (
+      <nav className="breadcrumb">
+        <span className="nav-item active">root</span>
+      </nav>
+    );
+  }
+
+  const parts = currentPath.split('/').filter(p => p);
+  const crumbs = parts.map((part, index) => ({
+    name: part,
+    fullPath: parts.slice(0, index + 1).join('/')
+  }));
+
+  return (
+    <nav className="breadcrumb">
+      <a
+        href="#"
+        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
+        title="Go to Root"
+      >
+        root
+      </a>
+      {crumbs.map((crumb, index) => {
+        const isLast = index === crumbs.length - 1;
+        return (
+          <React.Fragment key={crumb.fullPath}>
+            <span className="separator">/</span>
+            {isLast ? (
+              <span className="nav-item active">{crumb.name}</span>
+            ) : (
+              <a
+                href="#"
+                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
+              >
+                {crumb.name}
+              </a>
+            )}
+          </React.Fragment>
+        );
+      })}
+    </nav>
+  );
+}
+
+/**
+ * Component: FileViewer
+ * Shows file content inline with syntax highlighting
+ */
+function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const codeRef = useRef<HTMLElement>(null);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (content && codeRef.current) {
+      hljs.highlightElement(codeRef.current);
+    }
+  }, [content]);
+
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  const getLanguage = () => {
+    const ext = filename.split('.').pop()?.toLowerCase() || '';
+    const langMap: Record<string, string> = {
+      js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
+      py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
+      c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
+      sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
+      json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
+      html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss',
+      sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml',
+      dockerfile: 'dockerfile', makefile: 'makefile'
+    };
+    return langMap[ext] || 'plaintext';
+  };
+
+  const addLineNumbers = (text: string) => {
+    const lines = text.split('\n');
+    return lines.map((_, i) => i + 1).join('\n');
+  };
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <pre style={{ display: 'flex' }}>
+              <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span>
+              <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code>
+            </pre>
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: MarkdownViewerModal
+ */
+function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading || !wasmReady ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <div className="readme-content" ref={contentRef} />
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileList
+ */
+function FileList({ directories, files, onNavigate, onOpenFile }: {
+  directories: any[];
+  files: any[];
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const isEmpty = directories.length === 0 && files.length === 0;
+
+  if (isEmpty) {
+    return (
+      <div className="file-list-container">
+        <div className="empty-state">This directory is empty.</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="file-list-container">
+      <div className="file-header">Files</div>
+      <div id="fileListBody">
+        {directories.map((dir) => (
+          <FileRow
+            key={dir.abspath}
+            item={dir}
+            iconUrl={ICONS.folder}
+            isDir={true}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+        {files.map((file) => (
+          <FileRow
+            key={file.abspath}
+            item={file}
+            iconUrl={ICONS.file}
+            isDir={false}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileRow
+ */
+function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: {
+  item: { abspath: string; basename: string };
+  iconUrl: string;
+  isDir: boolean;
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const handleClick = (e: React.MouseEvent) => {
+    e.preventDefault();
+    if (isDir) {
+      onNavigate(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      onOpenFile(item.abspath);
+    } else {
+      window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
+    }
+  };
+
+  const handleMouseEnter = () => {
+    if (isDir) {
+      prefetchDirectory(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      prefetchFile(item.abspath);
+    }
+  };
+
+  const href = isDir
+    ? `#`
+    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
+
+  return (
+    <div className="file-row" onMouseEnter={handleMouseEnter}>
+      <span className="icon">
+        <img className="icon-invert" src={iconUrl} alt={isDir ? "Directory" : "File"} />
+      </span>
+      <span className="name">
+        <a href={href} onClick={handleClick}>
+          {item.basename}
+        </a>
+      </span>
+    </div>
+  );
+}
+
+/**
+ * Component: ReadmeViewer
+ */
+function ReadmeViewer({ content }: { content: string | null }) {
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  if (!content) return null;
+
+  return (
+    <div className="readme-section">
+      <div className="readme-header">
+        <img className="icon-invert" src={ICONS.file} width="16" alt="" style={{ opacity: 0.5 }} />
+        README.md
+      </div>
+      <div className="readme-content" ref={contentRef}>
+        {!wasmReady && 'Loading...'}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Directory Browser Component (no header/footer - for embedding in app)
+ */
+interface DirectoryBrowserProps {
+  initialPath?: string;
+  onPathChange?: (path: string) => void;
+}
+
+function DirectoryBrowser({ initialPath = '', onPathChange }: DirectoryBrowserProps) {
+  const [currentPath, setCurrentPath] = useState(initialPath);
+  const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] });
+  const [readme, setReadme] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [viewingFile, setViewingFile] = useState<string | null>(null);
+
+  // Sync with initialPath prop
+  useEffect(() => {
+    setCurrentPath(initialPath);
+  }, [initialPath]);
+
+  useEffect(() => {
+    fetchDirectory(currentPath);
+    fetchReadme(currentPath);
+  }, [currentPath]);
+
+  const navigate = useCallback((path: string) => {
+    setCurrentPath(path);
+    onPathChange?.(path);
+  }, [onPathChange]);
+
+  const fetchDirectory = async (path: string) => {
+    setLoading(true);
+    setError(null);
+    try {
+      const cacheKey = `dir:${path}`;
+      let data;
+      if (prefetchCache.has(cacheKey)) {
+        data = await prefetchCache.get(cacheKey);
+        prefetchCache.delete(cacheKey);
+      } else {
+        const url = path
+          ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+          : `${API_BASE}/list`;
+        const response = await fetch(url);
+        if (response.ok) {
+          data = await response.json();
+        }
+      }
+
+      if (data?.error) {
+        throw new Error(data.error);
+      }
+
+      setContent({
+        files: data?.files || [],
+        directories: data?.directories || []
+      });
+    } catch (err: any) {
+      console.error('Error loading directory:', err);
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchReadme = async (path: string) => {
+    setReadme(null);
+    const readmePath = path ? `${path}/README.md` : 'README.md';
+    try {
+      const response = await fetch(`${API_BASE}/file?path=${encodeURIComponent(readmePath)}`);
+      if (response.ok) {
+        const text = await response.text();
+        setReadme(text);
+      }
+    } catch (err) {
+      // Readme is optional
+    }
+  };
+
+  const handleOpenFile = useCallback((path: string) => {
+    setViewingFile(path);
+  }, []);
+
+  const handleCloseFile = useCallback(() => {
+    setViewingFile(null);
+  }, []);
+
+  return (
+    <>
+      <Breadcrumb currentPath={currentPath} onNavigate={navigate} />
+
+      {error && <div className="error-message">Error: {error}</div>}
+
+      {loading ? (
+        <div className="file-list-container">
+          <div className="loading-state">Loading files...</div>
+        </div>
+      ) : (
+        <>
+          <FileList
+            directories={content.directories}
+            files={content.files}
+            onNavigate={navigate}
+            onOpenFile={handleOpenFile}
+          />
+          <ReadmeViewer content={readme} />
+        </>
+      )}
+
+      {/* File Viewer Modal */}
+      {viewingFile && (
+        isMarkdownFile(viewingFile) ? (
+          <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
+        ) : (
+          <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
+        )
+      )}
+    </>
+  );
+}
+
+export { DirectoryBrowser };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/footer.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,31 @@
+import React from 'react';
+
+interface FooterProps {
+  showCloneUrl?: boolean;
+  cloneUrl?: string;
+}
+
+function Footer({
+  showCloneUrl = false,
+  cloneUrl = "hg clone http://zenbu.babocoder.com/repo",
+}: FooterProps) {
+  const currentYear = new Date().getFullYear();
+
+  return (
+    <footer className="footer">
+      {showCloneUrl && (
+        <div className="clone-box">
+          <div className="clone-box-inner">
+            <span className="clone-label">Clone HTTPS</span>
+            <code className="clone-url">{cloneUrl}</code>
+          </div>
+        </div>
+      )}
+      <div className="footer-content">
+        <span className="footer-text">&copy; 2026 June Park</span>
+      </div>
+    </footer>
+  );
+}
+
+export { Footer };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/graph.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,334 @@
+import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+
+// Configuration constants for the layout
+const rowHeight = 40;
+const colWidth = 20;
+const nodeRadius = 4.5;
+
+// --- Interfaces ---
+
+interface Changeset {
+  node: string;
+  date: [number, number];
+  desc: string;
+  branch: string;
+  bookmarks: string[];
+  tags: string[];
+  user: string;
+  phase: string;
+  col: number;
+  row: number;
+  color: number;
+  edges: Array<{
+    bcolor: string;
+    col: number;
+    color: number;
+    nextcol: number;
+    width: number;
+  }>;
+  parents: string[];
+}
+
+interface GraphData {
+  node: string;
+  changeset_count: number;
+  changesets: Changeset[];
+}
+
+interface UseGraphDataOptions {
+  initialCommit?: string | null;
+  graphTop?: string | null;
+}
+
+interface UseGraphDataResult {
+  data: GraphData | null;
+  loading: boolean;
+  error: string | null;
+  loadMore: () => void;
+  hasMore: boolean;
+  tip: string | null;
+  currentCommit: string | null;
+}
+
+// --- Hook Logic ---
+
+function useGraphData({ initialCommit = null, graphTop = null }: UseGraphDataOptions = {}): UseGraphDataResult {
+  const [data, setData] = useState<GraphData | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [tip, setTip] = useState<string | null>(graphTop);
+  const [currentCommit, setCurrentCommit] = useState<string | null>(initialCommit);
+  const [hasMore, setHasMore] = useState(true);
+
+  const fetchData = useCallback(async (commit: string | null, tipNode: string | null, append: boolean = false) => {
+    if (loading) return;
+    setLoading(true);
+    setError(null);
+
+    try {
+      const url = !commit 
+        ? `/api/graph/tip?style=json` 
+        : `/api/graph/${commit}?graphtop=${tipNode}&style=json`;
+
+      const response = await fetch(url);
+      if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
+      
+      const result: GraphData = await response.json();
+
+      setData(prev => {
+        if (append && prev) {
+          const existingNodes = new Set(prev.changesets.map(cs => cs.node));
+          const newChangesets = result.changesets.filter(cs => !existingNodes.has(cs.node));
+          
+          // Re-index rows to ensure they increment correctly for the canvas height
+          const startRow = prev.changesets.length;
+          const reindexed = newChangesets.map((cs, idx) => ({
+            ...cs,
+            row: startRow + idx
+          }));
+
+          return {
+            ...result,
+            changesets: [...prev.changesets, ...reindexed]
+          };
+        }
+        return result;
+      });
+
+      if (!tip && !append) setTip(result.node);
+
+      if (result.changesets.length > 0) {
+        const lastNode = result.changesets[result.changesets.length - 1].node;
+        setCurrentCommit(lastNode);
+        setHasMore(result.changesets.length >= 30);
+      } else {
+        setHasMore(false);
+      }
+    } catch (err: any) {
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  }, [tip, loading]);
+
+  useEffect(() => {
+    fetchData(initialCommit, graphTop, false);
+  }, [initialCommit, graphTop]);
+
+  const loadMore = useCallback(() => {
+    if (!loading && hasMore && currentCommit && tip) {
+      fetchData(currentCommit, tip, true);
+    }
+  }, [loading, hasMore, currentCommit, tip, fetchData]);
+
+  return { data, loading, error, loadMore, hasMore, tip, currentCommit };
+}
+
+// --- Pencil Rendering Logic ---
+
+const drawPencilLine = (
+  ctx: CanvasRenderingContext2D,
+  x1: number, y1: number,
+  x2: number, y2: number,
+  texture: CanvasPattern | null, // Ensure type safety
+  isCurve: boolean = false
+) => {
+  const strokes = 3;
+  ctx.save();
+
+  for (let s = 0; s < strokes; s++) {
+    ctx.beginPath();
+    ctx.strokeStyle = texture;
+    ctx.globalAlpha = 0.2 + (s * 0.2); 
+    ctx.lineWidth = 1.5 - (s * 0.2); // Pencil lines are usually thinner
+
+    // 2. Realistic Jitter: Actually return a random small number
+    const jitter = () => (Math.random() - 0.5) * 1.5;
+
+    ctx.moveTo(x1 + jitter(), y1 + jitter());
+
+    if (isCurve) {
+      const cpY = y1 + (y2 - y1) / 2;
+      ctx.bezierCurveTo(
+        x1 + jitter(), cpY + jitter(), 
+        x2 + jitter(), cpY + jitter(), 
+        x2 + jitter(), y2 + jitter()
+      );
+    } else {
+      ctx.lineTo(x2 + jitter(), y2 + jitter());
+    }
+    
+    ctx.stroke();
+  }
+  ctx.restore();
+}
+
+// --- Main Component ---
+
+interface GraphProps {
+  data: GraphData | null;
+  loading?: boolean;
+  hasMore?: boolean;
+  onLoadMore?: () => void;
+  onCommitClick?: (node: string) => void;
+  maxRows?: number;
+}
+
+const Graph = ({ data, loading, hasMore, onLoadMore, onCommitClick, maxRows }: GraphProps) => {
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  const changesets = useMemo(() => 
+    maxRows && data?.changesets ? data.changesets.slice(0, maxRows) : data?.changesets || [], [data, maxRows]);
+
+  let pencilPattern;
+  const img = new Image();
+  img.src = "http://localhost:6970/pencil_texture.png";
+
+  useEffect(() => {
+    const canvas = canvasRef.current;
+    if (!canvas || !changesets.length) return;
+
+    const ctx = canvas.getContext('2d');
+    if (!ctx) return;
+
+    // Grab colors from CSS variables or defaults
+    const getColors = () => {
+      const s = getComputedStyle(document.documentElement);
+      return [
+        s.getPropertyValue('--graph-1').trim() || '#4dabf7',
+        s.getPropertyValue('--graph-2').trim() || '#63e6be',
+        s.getPropertyValue('--graph-3').trim() || '#ffbc42',
+        s.getPropertyValue('--graph-4').trim() || '#b197fc',
+        s.getPropertyValue('--graph-5').trim() || '#ff8787',
+        s.getPropertyValue('--graph-6').trim() || '#f06595',
+      ];
+    };
+
+    const colors = getColors();
+    const dpr = window.devicePixelRatio || 1;
+    const maxCol = Math.max(...changesets.map(cs => cs.col), 0);
+    const canvasWidth = (maxCol + 2) * colWidth;
+    
+    // Scale for high-DPI screens
+    canvas.width = canvasWidth * dpr;
+    canvas.height = changesets.length * rowHeight * dpr;
+    canvas.style.width = `${canvasWidth}px`;
+    canvas.style.height = `${changesets.length * rowHeight}px`;
+    ctx.scale(dpr, dpr);
+    ctx.clearRect(0, 0, canvasWidth, changesets.length * rowHeight);
+
+    const getX = (col: number) => (col + 1) * colWidth;
+    const getY = (row: number) => (row * rowHeight) + (rowHeight / 2);
+
+    const renderCanvas = () => {
+      if (!pencilPattern) return; // Don't draw if the pattern isn't ready
+    
+      // Pass 1: Draw Connecting Edges
+      changesets.forEach((cs, i) => {
+        if (!cs.edges) return;
+        cs.edges.forEach(edge => {
+          const sX = getX(edge.col), sY = getY(i);
+          const eX = getX(edge.nextcol), eY = getY(i + 1);
+          
+          drawPencilLine(ctx, sX, sY, eX, eY, pencilPattern, edge.col !== edge.nextcol);
+        });
+      });
+
+      // Pass 2: Draw Commit Nodes
+      changesets.forEach((cs, i) => {
+        const x = getX(cs.col), y = getY(i);
+        const color = colors[cs.color % colors.length];
+
+        // Sketchy outer glow
+        ctx.beginPath();
+        ctx.arc(x, y, nodeRadius + 2, 0, Math.PI * 2);
+        ctx.fillStyle = `${color}33`;
+        ctx.fill();
+
+        // Core Node
+        ctx.beginPath();
+        ctx.arc(x, y, nodeRadius, 0, Math.PI * 2);
+        ctx.fillStyle = color;
+        ctx.fill();
+
+        // Sketchy border rings
+        for (let s = 0; s < 2; s++) {
+          ctx.beginPath();
+          ctx.arc(x + (Math.random() - 0.5), y + (Math.random() - 0.5), nodeRadius + 0.5, 0, Math.PI * 2);
+          ctx.strokeStyle = '#000000';
+          ctx.globalAlpha = 0.3;
+          ctx.lineWidth = 0.8;
+          ctx.stroke();
+        }
+        ctx.globalAlpha = 1;
+      });
+    };
+
+    img.onload = () => {
+      console.log("WTF");
+      pencilPattern = ctx.createPattern(img, "repeat")!;
+      renderCanvas(); 
+    };
+  }, [changesets]);
+
+  // Handle Infinite Scroll via Intersection Observer
+  useEffect(() => {
+    if (!onLoadMore || !hasMore) return;
+    const observer = new IntersectionObserver((entries) => {
+      if (entries[0].isIntersecting && !loading) {
+        onLoadMore();
+      }
+    }, { threshold: 0.1 });
+
+    const sentinel = document.getElementById('infinite-scroll-sentinel');
+    if (sentinel) observer.observe(sentinel);
+    return () => observer.disconnect();
+  }, [onLoadMore, hasMore, loading]);
+
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#1a1a1a', fontFamily: 'monospace' }}>
+      <div 
+        ref={containerRef} 
+        style={{ display: 'flex', flex: 1, overflowY: 'auto', position: 'relative' }}
+      >
+        {/* Graph Column - Sticky to keep lines aligned with text during scroll */}
+        <div style={{ position: 'sticky', top: 0, height: 'fit-content', zIndex: 10, borderRight: '1px solid #333' }}>
+          <canvas ref={canvasRef} style={{ display: 'block' }} />
+        </div>
+
+        {/* Details Column */}
+        <div style={{ flex: 1 }}>
+          {changesets.map((cs) => (
+            <div 
+              key={cs.node} 
+              style={{ 
+                height: rowHeight, 
+                display: 'flex', 
+                alignItems: 'center', 
+                padding: '0 15px', 
+                borderBottom: '1px solid #252525', 
+                cursor: 'pointer',
+                fontSize: '13px',
+                whiteSpace: 'nowrap'
+              }}
+              onClick={() => onCommitClick?.(cs.node)}
+              onMouseEnter={(e) => (e.currentTarget.style.background = '#222')}
+              onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
+            >
+              <span style={{ color: '#4dabf7', width: '90px', flexShrink: 0 }}>{cs.node.substring(0, 12)}</span>
+              <span style={{ color: '#eee', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', paddingRight: '20px' }}>{cs.desc}</span>
+              <span style={{ color: '#888', width: '150px', textAlign: 'right' }}>{cs.user.split(' <')[0]}</span>
+            </div>
+          ))}
+          <div id="infinite-scroll-sentinel" style={{ height: '50px' }} />
+        </div>
+      </div>
+      
+      {loading && <div style={{ padding: '10px', textAlign: 'center', color: '#888', fontSize: '12px', background: '#111' }}>Loading repository history...</div>}
+    </div>
+  );
+};
+
+export { Graph, useGraphData };
+export type { GraphData, Changeset, UseGraphDataResult };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/header.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,60 @@
+import React from 'react';
+
+// Icons
+const ICONS = {
+  repo: "/public/epi_all_colors.svg",
+};
+
+const SunIcon = () => (
+  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+    <circle cx="12" cy="12" r="5"/>
+    <line x1="12" y1="1" x2="12" y2="3"/>
+    <line x1="12" y1="21" x2="12" y2="23"/>
+    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
+    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
+    <line x1="1" y1="12" x2="3" y2="12"/>
+    <line x1="21" y1="12" x2="23" y2="12"/>
+    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
+    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
+  </svg>
+);
+
+const MoonIcon = () => (
+  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
+  </svg>
+);
+
+interface HeaderProps {
+  title?: string;
+  subtitle?: string;
+  showThemeToggle?: boolean;
+  isDark?: boolean;
+  onToggleTheme?: () => void;
+}
+
+function Header({
+  title = "Zenbu Repository",
+  subtitle,
+  showThemeToggle = true,
+  isDark = false,
+  onToggleTheme,
+}: HeaderProps) {
+  return (
+    <header className="header">
+      <img src={ICONS.repo} alt="Zenbu" className="header-icon" />
+      <div className="header-content">
+        <h1><a href="/">{title}</a></h1>
+        {subtitle && <p className="header-subtitle">{subtitle}</p>}
+      </div>
+      {showThemeToggle && onToggleTheme && (
+        <button className="theme-toggle" onClick={onToggleTheme} title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}>
+          {isDark ? <SunIcon /> : <MoonIcon />}
+          <span>{isDark ? 'Light' : 'Dark'}</span>
+        </button>
+      )}
+    </header>
+  );
+}
+
+export { Header };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/repo-browser.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,584 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js';
+import hljs from 'third_party/highlight/highlight.min.js';
+import { Header } from "hg-web/src/components/header";
+import { Footer } from "hg-web/src/components/footer";
+import { ThemeProvider, useTheme } from "hg-web/src/components/theme";
+
+// --- ICONS (served as static files) ---
+const ICONS = {
+  folder: "/icons/folder.png",
+  file: "/icons/file.svg",
+  close: "/icons/close.png"
+};
+
+const API_BASE = '/api/repo';
+
+// File extensions that should be displayed as code
+const CODE_EXTENSIONS = new Set([
+  'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp',
+  'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash',
+  'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss',
+  'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg',
+  'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
+  'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
+  'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample'
+]);
+
+// Prefetch cache
+const prefetchCache = new Map<string, Promise<any>>();
+
+function isCodeFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  const basename = filename.toLowerCase();
+  return CODE_EXTENSIONS.has(ext) ||
+         CODE_EXTENSIONS.has(basename) ||
+         basename === 'dockerfile' ||
+         basename === 'makefile' ||
+         basename.startsWith('.');
+}
+
+function isMarkdownFile(filename: string): boolean {
+  const ext = filename.split('.').pop()?.toLowerCase() || '';
+  return ext === 'md' || ext === 'markdown';
+}
+
+function prefetchDirectory(path: string): void {
+  const cacheKey = `dir:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  const url = path
+    ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+    : `${API_BASE}/list`;
+
+  prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null));
+}
+
+function prefetchFile(path: string): void {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) return;
+
+  prefetchCache.set(cacheKey,
+    fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`)
+      .then(r => r.ok ? r.text() : null)
+      .catch(() => null)
+  );
+}
+
+async function getCachedFile(path: string): Promise<string | null> {
+  const cacheKey = `file:${path}`;
+  if (prefetchCache.has(cacheKey)) {
+    return prefetchCache.get(cacheKey);
+  }
+  prefetchFile(path);
+  return prefetchCache.get(cacheKey)!;
+}
+
+/**
+ * Component: Breadcrumb
+ */
+function Breadcrumb({ currentPath, onNavigate }: { currentPath: string; onNavigate: (path: string) => void }) {
+  if (!currentPath) {
+    return (
+      <nav className="breadcrumb">
+        <span className="nav-item active">root</span>
+      </nav>
+    );
+  }
+
+  const parts = currentPath.split('/').filter(p => p);
+  const crumbs = parts.map((part, index) => ({
+    name: part,
+    fullPath: parts.slice(0, index + 1).join('/')
+  }));
+
+  return (
+    <nav className="breadcrumb">
+      <a
+        href="/"
+        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
+        title="Go to Root"
+      >
+        root
+      </a>
+      {crumbs.map((crumb, index) => {
+        const isLast = index === crumbs.length - 1;
+        return (
+          <React.Fragment key={crumb.fullPath}>
+            <span className="separator">/</span>
+            {isLast ? (
+              <span className="nav-item active">{crumb.name}</span>
+            ) : (
+              <a
+                href={`?path=${encodeURIComponent(crumb.fullPath)}`}
+                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
+              >
+                {crumb.name}
+              </a>
+            )}
+          </React.Fragment>
+        );
+      })}
+    </nav>
+  );
+}
+
+/**
+ * Component: FileViewer
+ * Shows file content inline with syntax highlighting
+ */
+function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const codeRef = useRef<HTMLElement>(null);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (content && codeRef.current) {
+      hljs.highlightElement(codeRef.current);
+    }
+  }, [content]);
+
+  // Close on escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  // Get language from file extension for highlight.js
+  const getLanguage = () => {
+    const ext = filename.split('.').pop()?.toLowerCase() || '';
+    const langMap: Record<string, string> = {
+      js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
+      py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
+      c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
+      sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
+      json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
+      html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss',
+      sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml',
+      dockerfile: 'dockerfile', makefile: 'makefile'
+    };
+    return langMap[ext] || 'plaintext';
+  };
+
+  const addLineNumbers = (text: string) => {
+    const lines = text.split('\n');
+    return lines.map((_, i) => i + 1).join('\n');
+  };
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <pre style={{ display: 'flex' }}>
+              <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span>
+              <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code>
+            </pre>
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: MarkdownViewerModal
+ * Shows markdown content rendered in a modal
+ */
+function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) {
+  const [content, setContent] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  const filename = filePath.split('/').pop() || filePath;
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    setLoading(true);
+    getCachedFile(filePath).then((text) => {
+      setContent(text);
+      setLoading(false);
+    });
+  }, [filePath]);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  // Close on escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  return (
+    <div className="file-viewer-overlay" onClick={onClose}>
+      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
+        <div className="file-viewer-header">
+          <span className="file-viewer-title">
+            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
+            {filename}
+          </span>
+          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
+            <img className="icon-invert" src={ICONS.close} alt="Close" />
+          </button>
+        </div>
+        <div className="file-viewer-content">
+          {loading || !wasmReady ? (
+            <div className="file-viewer-loading">Loading...</div>
+          ) : content ? (
+            <div className="readme-content" ref={contentRef} />
+          ) : (
+            <div className="file-viewer-loading">Unable to load file</div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileList
+ */
+function FileList({ directories, files, onNavigate, onOpenFile }: {
+  directories: any[];
+  files: any[];
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const isEmpty = directories.length === 0 && files.length === 0;
+
+  if (isEmpty) {
+    return (
+      <div className="file-list-container">
+        <div className="empty-state">This directory is empty.</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="file-list-container">
+      <div className="file-header">Files</div>
+
+      <div id="fileListBody">
+        {directories.map((dir) => (
+          <FileRow
+            key={dir.abspath}
+            item={dir}
+            iconUrl={ICONS.folder}
+            isDir={true}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+
+        {files.map((file) => (
+          <FileRow
+            key={file.abspath}
+            item={file}
+            iconUrl={ICONS.file}
+            isDir={false}
+            onNavigate={onNavigate}
+            onOpenFile={onOpenFile}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Component: FileRow
+ */
+function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: {
+  item: { abspath: string; basename: string };
+  iconUrl: string;
+  isDir: boolean;
+  onNavigate: (path: string) => void;
+  onOpenFile: (path: string) => void;
+}) {
+  const handleClick = (e: React.MouseEvent) => {
+    e.preventDefault();
+    if (isDir) {
+      onNavigate(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      onOpenFile(item.abspath);
+    } else {
+      window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
+    }
+  };
+
+  const handleMouseEnter = () => {
+    if (isDir) {
+      prefetchDirectory(item.abspath);
+    } else if (isCodeFile(item.basename)) {
+      prefetchFile(item.abspath);
+    }
+  };
+
+  const href = isDir
+    ? `?path=${encodeURIComponent(item.abspath)}`
+    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
+
+  return (
+    <div className="file-row" onMouseEnter={handleMouseEnter}>
+      <span className="icon">
+        <img className="icon-invert" src={iconUrl} alt={isDir ? "Directory" : "File"} />
+      </span>
+      <span className="name">
+        <a href={href} onClick={handleClick}>
+          {item.basename}
+        </a>
+      </span>
+    </div>
+  );
+}
+
+/**
+ * Component: ReadmeViewer
+ */
+function ReadmeViewer({ content }: { content: string | null }) {
+  const contentRef = useRef<HTMLDivElement>(null);
+  const moduleRef = useRef<any>(null);
+  const [wasmReady, setWasmReady] = useState(false);
+
+  useEffect(() => {
+    createMarkdownModule().then((Module: any) => {
+      moduleRef.current = Module;
+      setWasmReady(true);
+    });
+  }, []);
+
+  useEffect(() => {
+    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
+
+    const Module = moduleRef.current;
+    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
+    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
+
+    const ptr = markdownToHtmlPtr(content);
+    const html = Module.UTF8ToString(ptr);
+    markdownFree(ptr);
+    contentRef.current.innerHTML = html;
+  }, [content, wasmReady]);
+
+  if (!content) return null;
+
+  return (
+    <div className="readme-section">
+      <div className="readme-header">
+        <img className="icon-invert" src={ICONS.file} width="16" alt="" style={{ opacity: 0.5 }} />
+        README.md
+      </div>
+      <div className="readme-content" ref={contentRef}>
+        {!wasmReady && 'Loading...'}
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Repository Browser Content (uses theme context)
+ */
+function RepoBrowserContent() {
+  const [currentPath, setCurrentPath] = useState(getCurrentPath());
+  const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] });
+  const [readme, setReadme] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [viewingFile, setViewingFile] = useState<string | null>(null);
+
+  const { isDark, toggleTheme } = useTheme();
+
+  function getCurrentPath() {
+    const params = new URLSearchParams(window.location.search);
+    return params.get('path') || '';
+  }
+
+  useEffect(() => {
+    const handlePopState = () => setCurrentPath(getCurrentPath());
+    window.addEventListener('popstate', handlePopState);
+    return () => window.removeEventListener('popstate', handlePopState);
+  }, []);
+
+  useEffect(() => {
+    fetchDirectory(currentPath);
+    fetchReadme(currentPath);
+  }, [currentPath]);
+
+  const navigate = (path: string) => {
+    const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/';
+    window.history.pushState({ path }, '', newUrl);
+    setCurrentPath(path);
+  };
+
+  const fetchDirectory = async (path: string) => {
+    setLoading(true);
+    setError(null);
+    try {
+      const cacheKey = `dir:${path}`;
+      let data;
+      if (prefetchCache.has(cacheKey)) {
+        data = await prefetchCache.get(cacheKey);
+        prefetchCache.delete(cacheKey);
+      } else {
+        const url = path
+          ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
+          : `${API_BASE}/list`;
+        const response = await fetch(url);
+        if (response.ok) {
+          data = await response.json();
+        }
+      }
+
+      if (data?.error) {
+        throw new Error(data.error);
+      }
+
+      setContent({
+        files: data?.files || [],
+        directories: data?.directories || []
+      });
+    } catch (err: any) {
+      console.error('Error loading directory:', err);
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchReadme = async (path: string) => {
+    setReadme(null);
+    const readmePath = path ? `${path}/README.md` : 'README.md';
+    try {
+      const response = await fetch(`${API_BASE}/file?path=${encodeURIComponent(readmePath)}`);
+      if (response.ok) {
+        const text = await response.text();
+        setReadme(text);
+      }
+    } catch (err) {
+      // Readme is optional, ignore errors
+    }
+  };
+
+  const handleOpenFile = useCallback((path: string) => {
+    setViewingFile(path);
+  }, []);
+
+  const handleCloseFile = useCallback(() => {
+    setViewingFile(null);
+  }, []);
+
+  return (
+    <>
+      <div className="repo-container">
+        <Header
+          title="Zenbu Repository"
+          subtitle="Browse and manage the mercurial codebase"
+          showThemeToggle={true}
+          isDark={isDark}
+          onToggleTheme={toggleTheme}
+        />
+
+        {/* Clone Bar */}
+        <div className="clone-box">
+          <div className="clone-box-inner">
+            <span className="clone-label">Clone HTTPS</span>
+            <code className="clone-url">hg clone http://zenbu.babocoder.com/repo</code>
+          </div>
+        </div>
+
+        {/* Navigation & Content */}
+        <Breadcrumb currentPath={currentPath} onNavigate={navigate} />
+
+        {error && <div className="error-message">Error: {error}</div>}
+
+        {loading ? (
+          <div className="file-list-container">
+            <div className="loading-state">Loading files...</div>
+          </div>
+        ) : (
+          <>
+            <FileList
+              directories={content.directories}
+              files={content.files}
+              onNavigate={navigate}
+              onOpenFile={handleOpenFile}
+            />
+            <ReadmeViewer content={readme} />
+          </>
+        )}
+
+        <Footer />
+      </div>
+
+      {/* File Viewer Modal */}
+      {viewingFile && (
+        isMarkdownFile(viewingFile) ? (
+          <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
+        ) : (
+          <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
+        )
+      )}
+    </>
+  );
+}
+
+/**
+ * Main Application Component with ThemeProvider
+ */
+function RepoBrowser() {
+  return (
+    <ThemeProvider>
+      <RepoBrowserContent />
+    </ThemeProvider>
+  );
+}
+
+export { RepoBrowser };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/components/theme.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -0,0 +1,72 @@
+import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
+
+interface ThemeContextType {
+  isDark: boolean;
+  toggleTheme: () => void;
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+// Apply theme class to document root
+function applyTheme(isDark: boolean) {
+  const root = document.documentElement;
+  if (isDark) {
+    root.classList.add('dark');
+    root.classList.remove('light');
+  } else {
+    root.classList.add('light');
+    root.classList.remove('dark');
+  }
+}
+
+interface ThemeProviderProps {
+  children: ReactNode;
+}
+
+function ThemeProvider({ children }: ThemeProviderProps) {
+  const [isDark, setIsDark] = useState(() => {
+    const saved = localStorage.getItem('theme');
+    if (saved) return saved === 'dark';
+    return window.matchMedia('(prefers-color-scheme: dark)').matches;
+  });
+
+  // Apply theme on mount and change
+  useEffect(() => {
+    applyTheme(isDark);
+    localStorage.setItem('theme', isDark ? 'dark' : 'light');
+  }, [isDark]);
+
+  // Listen for system theme changes
+  useEffect(() => {
+    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+    const handleChange = (e: MediaQueryListEvent) => {
+      // Only apply if no explicit preference is saved
+      if (!localStorage.getItem('theme')) {
+        setIsDark(e.matches);
+      }
+    };
+
+    mediaQuery.addEventListener('change', handleChange);
+    return () => mediaQuery.removeEventListener('change', handleChange);
+  }, []);
+
+  const toggleTheme = useCallback(() => {
+    setIsDark(prev => !prev);
+  }, []);
+
+  return (
+    <ThemeContext.Provider value={{ isDark, toggleTheme }}>
+      {children}
+    </ThemeContext.Provider>
+  );
+}
+
+function useTheme(): ThemeContextType {
+  const context = useContext(ThemeContext);
+  if (context === undefined) {
+    throw new Error('useTheme must be used within a ThemeProvider');
+  }
+  return context;
+}
+
+export { ThemeProvider, useTheme };
--- a/hg-web/src/index.css	Sat Jan 24 21:52:14 2026 -0800
+++ b/hg-web/src/index.css	Sun Jan 25 20:04:55 2026 -0800
@@ -1,179 +1,751 @@
+/* ===========================================
+   Component Styles
+   Import base.css before this file
+   =========================================== */
+
+/* ===========================================
+   App Layout
+   =========================================== */
+.app-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 40px 20px;
+}
+
+/* ===========================================
+   Header
+   =========================================== */
 .header {
-  border-bottom: 1px solid var(--border);
-  padding-bottom: 1rem;
-  margin-bottom: 2rem;
+  display: flex;
+  align-items: center;
+  margin-bottom: 24px;
+  gap: 15px;
+}
+
+.header-icon {
+  width: 32px;
+  height: 32px;
+  opacity: 0.8;
+  cursor: pointer;
 }
 
 .header h1 {
-  margin-bottom: 0.5rem;
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.header h1 a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.header-subtitle {
+  color: var(--text-secondary);
+  margin: 0;
+  font-size: 14px;
 }
 
-.header .description {
-  color: var(--secondary);
-  font-size: 0.95rem;
+/* ===========================================
+   Navigation Tabs
+   =========================================== */
+.nav-tabs {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 24px;
+  border-bottom: 1px solid var(--border);
+  padding-bottom: 8px;
 }
 
-.clone-info {
-  background: var(--code-bg);
-  border: 1px solid var(--border);
+.nav-tab {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 16px;
   border-radius: 6px;
-  padding: 1rem;
-  margin-bottom: 2rem;
+  background: transparent;
+  border: 1px solid transparent;
+  color: var(--text-secondary);
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.2s;
+}
+
+.nav-tab:hover {
+  background: var(--bg-subtle);
+  color: var(--text-primary);
+}
+
+.nav-tab.active {
+  background: var(--bg-subtle);
+  border-color: var(--border);
+  color: var(--text-primary);
+  font-weight: 500;
 }
 
-.clone-info code {
-  background: none;
-  color: var(--fg);
-  font-size: 0.95rem;
+.nav-tab svg {
+  color: var(--text-secondary);
+}
+
+/* ===========================================
+   Landing Page
+   =========================================== */
+.landing-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 24px;
+}
+
+@media (max-width: 768px) {
+  .landing-grid {
+    grid-template-columns: 1fr;
+  }
+}
+
+.landing-section {
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  overflow: hidden;
 }
 
-.breadcrumb {
-  margin-bottom: 1.5rem;
-  font-size: 0.95rem;
+.landing-section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
 }
 
-.breadcrumb a {
-  color: var(--link);
+.landing-section-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 600;
+  font-size: 14px;
+  color: var(--text-primary);
 }
 
-.breadcrumb a:hover {
+.landing-section-title svg {
+  color: var(--text-secondary);
+}
+
+.landing-section-link {
+  font-size: 12px;
+  color: var(--accent);
+  text-decoration: none;
+  padding: 4px 8px;
+  border-radius: 4px;
+  transition: background 0.2s;
+}
+
+.landing-section-link:hover {
+  background: var(--bg-subtle);
   text-decoration: underline;
 }
 
-.breadcrumb span {
-  color: var(--secondary);
-  margin: 0 0.5rem;
+.landing-section-content {
+  padding: 0;
+}
+
+/* ===========================================
+   Directory Items (Landing Preview)
+   =========================================== */
+.dir-item {
+  display: flex;
+  align-items: center;
+  padding: 10px 16px;
+  border-bottom: 1px solid var(--border);
+  font-size: 14px;
+  cursor: pointer;
+  transition: background 0.1s;
+}
+
+.dir-item:last-child {
+  border-bottom: none;
+}
+
+.dir-item:hover {
+  background: var(--hover);
+}
+
+.dir-item-icon {
+  margin-right: 12px;
+  opacity: 0.7;
+}
+
+.dir-item-icon img {
+  width: 18px;
+  height: 18px;
+}
+
+.dir-item-name {
+  color: var(--text-primary);
+}
+
+.dir-item-name:hover {
+  color: var(--accent);
+}
+
+/* ===========================================
+   Page Header (Graph Page)
+   =========================================== */
+.page-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 16px;
 }
 
-.file-list {
+.back-button {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  background: var(--bg-subtle);
   border: 1px solid var(--border);
   border-radius: 6px;
+  color: var(--text-secondary);
+  cursor: pointer;
+  font-size: 13px;
+  transition: all 0.2s;
+}
+
+.back-button:hover {
+  background: var(--hover);
+  color: var(--text-primary);
+}
+
+.page-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+/* ===========================================
+   Graph Params
+   =========================================== */
+.graph-params {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 16px;
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.graph-param {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 8px;
+  background: var(--bg-subtle);
+  border-radius: 4px;
+}
+
+.graph-param-label {
+  font-weight: 500;
+}
+
+.graph-param-value {
+  font-family: monospace;
+  color: var(--accent);
+}
+
+/* ===========================================
+   Graph Component
+   =========================================== */
+.graph-container {
+  background: var(--bg);
+  color: var(--text-primary);
+  font-family: "More Thin", sans-serif;
+  border-radius: 6px;
+  border: 1px solid var(--border);
   overflow: hidden;
 }
 
-.file-item {
+.graph-wrapper {
   display: flex;
-  align-items: center;
-  padding: 0.75rem 1rem;
-  border-bottom: 1px solid var(--border);
-  transition: background-color 0.2s;
+  align-items: flex-start;
+  max-height: 600px;
+  overflow-y: auto;
+}
+
+.graph-canvas-column {
+  flex-shrink: 0;
+  background: var(--bg);
+  position: sticky;
+  left: 0;
 }
 
-.file-item:last-child {
-  border-bottom: none;
+.graph-details-column {
+  flex-grow: 1;
+  overflow-x: hidden;
 }
 
-.file-item:hover {
+.graph-row {
+  height: 40px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  padding: 0 12px;
+  border-bottom: 1px solid var(--border);
+  font-size: 12px;
+  cursor: pointer;
+  transition: background-color 0.1s;
+}
+
+.graph-row:hover {
   background: var(--hover);
 }
 
-.file-item .icon {
-  margin-right: 0.75rem;
-  font-size: 1.2rem;
-  width: 20px;
-  text-align: center;
+.graph-row-meta {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 2px;
+  align-items: center;
+}
+
+.graph-hash {
+  color: var(--accent);
+  font-family: monospace;
+}
+
+.graph-user {
+  color: var(--text-secondary);
+  font-weight: 500;
+}
+
+.graph-branch {
+  color: var(--text-secondary);
+  font-size: 10px;
+  background: var(--bg-subtle);
+  padding: 1px 6px;
+  border-radius: 3px;
+}
+
+.graph-desc {
+  color: var(--text-primary);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.graph-badge-tip {
+  background: var(--success);
+  color: #fff;
+  padding: 0 4px;
+  border-radius: 2px;
+  font-size: 10px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.graph-badge-tag {
+  background: var(--accent);
+  color: #fff;
+  padding: 0 4px;
+  border-radius: 2px;
+  font-size: 10px;
+  font-weight: bold;
+  flex-shrink: 0;
 }
 
-.file-item .name {
-  flex: 1;
-  font-family: 'Monaco', 'Courier New', monospace;
-  font-size: 0.9rem;
+.graph-loading-row {
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--text-secondary);
+  font-size: 12px;
+}
+
+/* ===========================================
+   Common States
+   =========================================== */
+.empty-state {
+  padding: 40px;
+  text-align: center;
+  color: var(--text-secondary);
+}
+
+.loading-state {
+  padding: 40px;
+  text-align: center;
+  color: var(--text-secondary);
 }
 
-.file-item .name a {
-  color: var(--fg);
+.error-message {
+  padding: 15px;
+  border: 1px solid var(--danger-border);
+  background: var(--danger-bg);
+  color: var(--danger);
+  border-radius: 6px;
+  margin-bottom: 20px;
+}
+
+/* ===========================================
+   Repository Browser
+   =========================================== */
+.repo-container {
+  font-family: "More Thin", sans-serif;
+  max-width: 980px;
+  margin: 40px auto;
+  color: var(--text-primary);
+  padding: 0 20px;
+}
+
+/* Clone Box */
+.clone-box {
+  background: var(--bg-subtle);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 12px 16px;
+  margin-bottom: 24px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 }
 
-.file-item .name a:hover {
-  color: var(--link);
+.clone-label {
+  font-weight: 600;
+  font-size: 13px;
+  margin-right: 10px;
+  color: var(--text-primary);
+}
+
+.clone-url {
+  font-family: "More Thin", sans-serif;
+  background: var(--bg);
+  border: 1px solid var(--border);
+  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;
 }
 
-.file-item.directory .icon {
+.breadcrumb a {
   color: var(--accent);
+  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 */
+.file-list-container {
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  overflow: hidden;
+  background: var(--bg);
 }
 
-.file-item.file .icon {
-  color: var(--secondary);
+.file-header {
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
+  padding: 12px 16px;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-secondary);
+}
+
+.file-row {
+  display: flex;
+  align-items: center;
+  padding: 10px 16px;
+  border-bottom: 1px solid var(--border);
+  transition: background 0.1s;
+}
+
+.file-row:last-child {
+  border-bottom: none;
+}
+
+.file-row:hover {
+  background: var(--hover);
 }
 
-.readme-section {
-  margin-top: 2rem;
-  padding-top: 2rem;
-  border-top: 1px solid var(--border);
+.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;
 }
 
-.readme-section h2 {
-  margin-bottom: 1rem;
-  font-size: 1.5rem;
+.file-row .name a:hover {
+  color: var(--accent);
+  text-decoration: underline;
+}
+
+/* Readme */
+.readme-section {
+  margin-top: 32px;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+}
+
+.readme-header {
+  background: var(--bg-subtle);
+  padding: 10px 16px;
+  font-size: 12px;
+  font-weight: 600;
+  border-bottom: 1px solid var(--border);
+  display: flex;
+  align-items: center;
+  gap: 8px;
 }
 
 .readme-content {
+  padding: 32px;
+  background: var(--bg);
+  overflow-x: auto;
+  color: var(--text-primary);
+}
+
+/* File Viewer Modal */
+.file-viewer-overlay {
+  position: fixed;
+  inset: 0;
+  background: var(--overlay);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+  padding: 20px;
+}
+
+.file-viewer {
+  background: var(--bg);
   border: 1px solid var(--border);
   border-radius: 6px;
-  padding: 1.5rem;
-  background: var(--code-bg);
+  width: 100%;
+  max-width: 900px;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+.file-viewer-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  background: var(--bg-subtle);
+  border-bottom: 1px solid var(--border);
+  border-radius: 6px 6px 0 0;
+}
+
+.file-viewer-title {
+  font-weight: 600;
+  font-size: 14px;
+  color: var(--text-primary);
+  display: flex;
+  align-items: center;
+  gap: 8px;
 }
 
-.readme-content h1 { font-size: 1.75rem; margin-top: 1.5rem; }
-.readme-content h2 { font-size: 1.5rem; margin-top: 1.25rem; }
-.readme-content h3 { font-size: 1.25rem; margin-top: 1rem; }
+.file-viewer-close {
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  padding: 4px;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
 
-.readme-content h1:first-child,
-.readme-content h2:first-child,
-.readme-content h3:first-child {
-  margin-top: 0;
+.file-viewer-close:hover {
+  background: var(--hover);
+}
+
+.file-viewer-close img {
+  width: 16px;
+  height: 16px;
+  opacity: 0.7;
+}
+
+.file-viewer-content {
+  overflow: auto;
+  flex: 1;
 }
 
-.readme-content ul,
-.readme-content ol {
-  margin-left: 2rem;
-  margin-bottom: 1rem;
+.file-viewer-content pre {
+  margin: 0;
+  padding: 16px;
+  background: var(--bg-code);
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  overflow-x: auto;
+}
+
+.file-viewer-content code {
+  background: transparent;
+  padding: 0;
 }
 
-.readme-content li {
-  margin-bottom: 0.5rem;
+.file-viewer-loading {
+  padding: 40px;
+  text-align: center;
+  color: var(--text-secondary);
 }
 
-.readme-content img {
-  max-width: 100%;
-  height: auto;
-  border-radius: 6px;
+.file-viewer-line-numbers {
+  display: inline-block;
+  user-select: none;
+  text-align: right;
+  padding-right: 16px;
+  margin-right: 16px;
+  border-right: 1px solid var(--border);
+  color: var(--text-secondary);
+  opacity: 0.5;
 }
 
-.empty-state {
-  text-align: center;
-  padding: 3rem 1rem;
-  color: var(--secondary);
+/* Theme Toggle Button */
+.theme-toggle {
+  margin-left: auto;
+  background: var(--bg-subtle);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 8px 12px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  color: var(--text-secondary);
+  font-size: 13px;
+  transition: all 0.2s;
 }
 
-.error-message {
-  background: var(--danger);
-  color: white;
-  padding: 1rem;
-  border-radius: 6px;
-  margin-bottom: 1rem;
+.theme-toggle:hover {
+  background: var(--hover);
+  color: var(--text-primary);
+}
+
+.theme-toggle svg {
+  flex-shrink: 0;
+}
+
+/* Description */
+.description {
+  color: var(--text-secondary);
+  margin: 0;
+  font-size: 14px;
 }
 
-/* Mobile responsive */
+/* ===========================================
+   Mobile Responsive
+   =========================================== */
 @media (max-width: 768px) {
-  main {
-    padding: 1rem;
+  .app-container {
+    padding: 20px 15px;
   }
 
-  .file-item {
-    padding: 0.5rem 0.75rem;
+  .repo-container {
+    padding: 0 15px;
+  }
+
+  .file-row {
+    padding: 8px 12px;
   }
 
-  .file-item .name {
-    font-size: 0.85rem;
-  }
-
-  .clone-info {
-    padding: 0.75rem;
-    overflow-x: auto;
-  }
-
-  .readme-content {
-    padding: 1rem;
+  .clone-box {
+    padding: 10px 12px;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
   }
 }
+
+/* ===========================================
+   Footer
+   =========================================== */
+.footer {
+  margin-top: 48px;
+  padding-top: 24px;
+  border-top: 1px solid var(--border);
+}
+
+.footer-content {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 16px 0;
+  color: var(--text-secondary);
+  font-size: 13px;
+}
+
+.footer-separator {
+  opacity: 0.5;
+}
+
+.footer-text {
+  color: var(--text-secondary);
+}
+
+.clone-box-inner {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+
+/* Header content wrapper */
+.header-content {
+  flex: 1;
+}
+
+.header-content h1 {
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.header-content h1 a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.header-content h1 a:hover {
+  text-decoration: none;
+}
--- a/hg-web/src/index.html	Sat Jan 24 21:52:14 2026 -0800
+++ b/hg-web/src/index.html	Sun Jan 25 20:04:55 2026 -0800
@@ -4,8 +4,13 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Zenbu Repository</title>
+    <link rel="stylesheet" href="/index.css">
+    <link rel="stylesheet" href="/base.css">
+
     <link rel="stylesheet" href="/a11y-dark.min.css"  media="(prefers-color-scheme: dark)">
     <link rel="stylesheet" href="/a11y-light.min.css" media="(prefers-color-scheme: light)">
+
+    <link rel="preload" href="/public/fonts/more-sugar.regular.otf" as="font" type="font/otf" crossorigin>
     <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
   </head>
   <body>
--- a/hg-web/src/main.tsx	Sat Jan 24 21:52:14 2026 -0800
+++ b/hg-web/src/main.tsx	Sun Jan 25 20:04:55 2026 -0800
@@ -1,8 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
-import { RepoBrowser } from "hg-web/src/repo-browser";
+import { App } from "hg-web/src/components/app";
 
-const root = ReactDOM.createRoot(document.getElementById('root'));
-
-// Use JSX syntax (<RepoBrowser />) 
-root.render(<RepoBrowser />);
+const root = ReactDOM.createRoot(document.getElementById('root')!);
+root.render(<App />);
Binary file hg-web/src/pencil_texture.png has changed
--- a/hg-web/src/repo-browser.tsx	Sat Jan 24 21:52:14 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,879 +0,0 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js';
-import hljs from 'third_party/highlight/highlight.min.js';
-
-// --- ICONS (served as static files) ---
-const ICONS = {
-  folder: "/icons/folder.png",
-  file: "/icons/file.svg",
-  home: "/icons/home.png",
-  repo: "/public/epi_all_colors.svg",
-  close: "/icons/close.png"
-};
-
-// SVG Icons for theme toggle
-const SunIcon = ({ color = "currentColor" }: { color?: string }) => (
-  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-    <circle cx="12" cy="12" r="5"/>
-    <line x1="12" y1="1" x2="12" y2="3"/>
-    <line x1="12" y1="21" x2="12" y2="23"/>
-    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
-    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
-    <line x1="1" y1="12" x2="3" y2="12"/>
-    <line x1="21" y1="12" x2="23" y2="12"/>
-    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
-    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
-  </svg>
-);
-
-const MoonIcon = ({ color = "currentColor" }: { color?: string }) => (
-  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
-  </svg>
-);
-
-const API_BASE = '/api/repo';
-
-// File extensions that should be displayed as code
-const CODE_EXTENSIONS = new Set([
-  'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp',
-  'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash',
-  'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss',
-  'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg',
-  'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
-  'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
-  'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample'
-]);
-
-// Prefetch cache
-const prefetchCache = new Map<string, Promise<any>>();
-
-function isCodeFile(filename: string): boolean {
-  const ext = filename.split('.').pop()?.toLowerCase() || '';
-  const basename = filename.toLowerCase();
-  return CODE_EXTENSIONS.has(ext) ||
-         CODE_EXTENSIONS.has(basename) ||
-         basename === 'dockerfile' ||
-         basename === 'makefile' ||
-         basename.startsWith('.');
-}
-
-function isMarkdownFile(filename: string): boolean {
-  const ext = filename.split('.').pop()?.toLowerCase() || '';
-  return ext === 'md' || ext === 'markdown';
-}
-
-function prefetchDirectory(path: string): void {
-  const cacheKey = `dir:${path}`;
-  if (prefetchCache.has(cacheKey)) return;
-
-  const url = path
-    ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
-    : `${API_BASE}/list`;
-
-  prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null));
-}
-
-function prefetchFile(path: string): void {
-  const cacheKey = `file:${path}`;
-  if (prefetchCache.has(cacheKey)) return;
-
-  prefetchCache.set(cacheKey,
-    fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`)
-      .then(r => r.ok ? r.text() : null)
-      .catch(() => null)
-  );
-}
-
-async function getCachedFile(path: string): Promise<string | null> {
-  const cacheKey = `file:${path}`;
-  if (prefetchCache.has(cacheKey)) {
-    return prefetchCache.get(cacheKey);
-  }
-  prefetchFile(path);
-  return prefetchCache.get(cacheKey)!;
-}
-
-/**
- * Component: Styles
- * Injected CSS for the polished look with dark/light mode support
- */
-const GlobalStyles = ({ isDark }: { isDark: boolean }) => (
-  <style>{`
-    :root {
-      --bg-color: ${isDark ? '#0d1117' : '#ffffff'};
-      --bg-subtle: ${isDark ? '#161b22' : '#f6f8fa'};
-      --bg-code: ${isDark ? '#1c2128' : '#f6f8fa'};
-      --border-color: ${isDark ? '#30363d' : '#d0d7de'};
-      --accent-color: ${isDark ? '#58a6ff' : '#0969da'};
-      --text-primary: ${isDark ? '#e6edf3' : '#1f2328'};
-      --text-secondary: ${isDark ? '#8b949e' : '#656d76'};
-      --hover-color: ${isDark ? '#1c2128' : '#f3f4f6'};
-      --radius: 6px;
-      --code-bg: ${isDark ? '#161b22' : '#ffffff'};
-    }
-
-    body {
-      background: var(--bg-color);
-      transition: background 0.2s;
-    }
-
-    .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; }
-
-    /* Theme Toggle */
-    .theme-toggle {
-      margin-left: auto;
-      background: var(--bg-subtle);
-      border: 1px solid var(--border-color);
-      border-radius: var(--radius);
-      padding: 8px 12px;
-      cursor: pointer;
-      display: flex;
-      align-items: center;
-      gap: 6px;
-      color: var(--text-secondary);
-      font-size: 13px;
-      transition: all 0.2s;
-    }
-    .theme-toggle:hover {
-      background: var(--hover-color);
-      color: var(--text-primary);
-    }
-    .theme-toggle svg {
-      flex-shrink: 0;
-    }
-
-    /* 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: var(--bg-color);
-      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;
-      background: var(--bg-color);
-    }
-    .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 ${isDark ? '#f8514966' : '#ffdce0'};
-      background: ${isDark ? '#f8514926' : '#ffebe9'}; color: ${isDark ? '#f85149' : '#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;
-      filter: ${isDark ? 'invert(0.8)' : 'none'};
-    }
-    .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;
-    }
-    .readme-header img {
-      filter: ${isDark ? 'invert(0.7)' : 'none'};
-    }
-    #readmeContent { padding: 32px; background: var(--bg-color); overflow-x: auto; color: var(--text-primary); }
-
-    /* File Viewer */
-    .file-viewer-overlay {
-      position: fixed;
-      inset: 0;
-      background: ${isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.5)'};
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      z-index: 1000;
-      padding: 20px;
-    }
-    .file-viewer {
-      background: var(--bg-color);
-      border: 1px solid var(--border-color);
-      border-radius: var(--radius);
-      width: 100%;
-      max-width: 900px;
-      max-height: 90vh;
-      display: flex;
-      flex-direction: column;
-      box-shadow: 0 8px 32px rgba(0,0,0,0.3);
-    }
-    .file-viewer-header {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      padding: 12px 16px;
-      background: var(--bg-subtle);
-      border-bottom: 1px solid var(--border-color);
-      border-radius: var(--radius) var(--radius) 0 0;
-    }
-    .file-viewer-title {
-      font-weight: 600;
-      font-size: 14px;
-      color: var(--text-primary);
-      display: flex;
-      align-items: center;
-      gap: 8px;
-    }
-    .file-viewer-close {
-      background: transparent;
-      border: none;
-      cursor: pointer;
-      padding: 4px;
-      border-radius: 4px;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-    .file-viewer-close:hover {
-      background: var(--hover-color);
-    }
-    .file-viewer-close img {
-      width: 16px;
-      height: 16px;
-      filter: ${isDark ? 'invert(0.7)' : 'none'};
-      opacity: 0.7;
-    }
-    .file-viewer-content {
-      overflow: auto;
-      flex: 1;
-    }
-    .file-viewer-content pre {
-      margin: 0;
-      padding: 16px;
-      background: var(--code-bg);
-      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
-      font-size: 13px;
-      line-height: 1.5;
-      overflow-x: auto;
-    }
-    .file-viewer-content code {
-      background: transparent;
-      padding: 0;
-    }
-    .file-viewer-loading {
-      padding: 40px;
-      text-align: center;
-      color: var(--text-secondary);
-    }
-    .file-viewer-line-numbers {
-      display: inline-block;
-      user-select: none;
-      text-align: right;
-      padding-right: 16px;
-      margin-right: 16px;
-      border-right: 1px solid var(--border-color);
-      color: var(--text-secondary);
-      opacity: 0.5;
-    }
-  `}</style>
-);
-
-/**
- * Component: Breadcrumb
- */
-function Breadcrumb({ currentPath, onNavigate }) {
-  if (!currentPath) {
-    return (
-      <nav id="breadcrumb">
-        <span className="nav-item active">root</span>
-      </nav>
-    );
-  }
-
-  const parts = currentPath.split('/').filter(p => p);
-  const crumbs = parts.map((part, index) => ({
-    name: part,
-    fullPath: parts.slice(0, index + 1).join('/')
-  }));
-
-  return (
-    <nav id="breadcrumb">
-      <a 
-        href="/" 
-        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
-        title="Go to Root"
-      >
-        root
-      </a>
-      {crumbs.map((crumb, index) => {
-        const isLast = index === crumbs.length - 1;
-        return (
-          <React.Fragment key={crumb.fullPath}>
-            <span className="separator">/</span>
-            {isLast ? (
-              <span className="nav-item active">{crumb.name}</span>
-            ) : (
-              <a 
-                href={`?path=${encodeURIComponent(crumb.fullPath)}`}
-                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
-              >
-                {crumb.name}
-              </a>
-            )}
-          </React.Fragment>
-        );
-      })}
-    </nav>
-  );
-}
-
-/**
- * Component: FileViewer
- * Shows file content inline with syntax highlighting
- */
-function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
-  const [content, setContent] = useState<string | null>(null);
-  const [loading, setLoading] = useState(true);
-  const codeRef = useRef<HTMLElement>(null);
-
-  const filename = filePath.split('/').pop() || filePath;
-
-  useEffect(() => {
-    setLoading(true);
-    getCachedFile(filePath).then((text) => {
-      setContent(text);
-      setLoading(false);
-    });
-  }, [filePath]);
-
-  useEffect(() => {
-    if (content && codeRef.current) {
-      hljs.highlightElement(codeRef.current);
-    }
-  }, [content]);
-
-  // Close on escape key
-  useEffect(() => {
-    const handleKeyDown = (e: KeyboardEvent) => {
-      if (e.key === 'Escape') onClose();
-    };
-    window.addEventListener('keydown', handleKeyDown);
-    return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [onClose]);
-
-  // Get language from file extension for highlight.js
-  const getLanguage = () => {
-    const ext = filename.split('.').pop()?.toLowerCase() || '';
-    const langMap: Record<string, string> = {
-      js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
-      py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
-      c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
-      sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
-      json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
-      html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss',
-      sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml',
-      dockerfile: 'dockerfile', makefile: 'makefile'
-    };
-    return langMap[ext] || 'plaintext';
-  };
-
-  const addLineNumbers = (text: string) => {
-    const lines = text.split('\n');
-    return lines.map((_, i) => i + 1).join('\n');
-  };
-
-  return (
-    <div className="file-viewer-overlay" onClick={onClose}>
-      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
-        <div className="file-viewer-header">
-          <span className="file-viewer-title">
-            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
-            {filename}
-          </span>
-          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
-            <img src={ICONS.close} alt="Close" />
-          </button>
-        </div>
-        <div className="file-viewer-content">
-          {loading ? (
-            <div className="file-viewer-loading">Loading...</div>
-          ) : content ? (
-            <pre style={{ display: 'flex' }}>
-              <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span>
-              <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code>
-            </pre>
-          ) : (
-            <div className="file-viewer-loading">Unable to load file</div>
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}
-
-/**
- * Component: MarkdownViewerModal
- * Shows markdown content rendered in a modal
- */
-function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) {
-  const [content, setContent] = useState<string | null>(null);
-  const [loading, setLoading] = useState(true);
-  const contentRef = useRef<HTMLDivElement>(null);
-  const moduleRef = useRef<any>(null);
-  const [wasmReady, setWasmReady] = useState(false);
-
-  const filename = filePath.split('/').pop() || filePath;
-
-  useEffect(() => {
-    createMarkdownModule().then((Module: any) => {
-      moduleRef.current = Module;
-      setWasmReady(true);
-    });
-  }, []);
-
-  useEffect(() => {
-    setLoading(true);
-    getCachedFile(filePath).then((text) => {
-      setContent(text);
-      setLoading(false);
-    });
-  }, [filePath]);
-
-  useEffect(() => {
-    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
-
-    const Module = moduleRef.current;
-    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
-    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
-
-    const ptr = markdownToHtmlPtr(content);
-    const html = Module.UTF8ToString(ptr);
-    markdownFree(ptr);
-    contentRef.current.innerHTML = html;
-  }, [content, wasmReady]);
-
-  // Close on escape key
-  useEffect(() => {
-    const handleKeyDown = (e: KeyboardEvent) => {
-      if (e.key === 'Escape') onClose();
-    };
-    window.addEventListener('keydown', handleKeyDown);
-    return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [onClose]);
-
-  return (
-    <div className="file-viewer-overlay" onClick={onClose}>
-      <div className="file-viewer" onClick={(e) => e.stopPropagation()}>
-        <div className="file-viewer-header">
-          <span className="file-viewer-title">
-            <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
-            {filename}
-          </span>
-          <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
-            <img src={ICONS.close} alt="Close" />
-          </button>
-        </div>
-        <div className="file-viewer-content">
-          {loading || !wasmReady ? (
-            <div className="file-viewer-loading">Loading...</div>
-          ) : content ? (
-            <div id="readmeContent" ref={contentRef} style={{ padding: 32 }} />
-          ) : (
-            <div className="file-viewer-loading">Unable to load file</div>
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}
-
-/**
- * Component: FileList
- */
-function FileList({ directories, files, onNavigate, onOpenFile }: {
-  directories: any[];
-  files: any[];
-  onNavigate: (path: string) => void;
-  onOpenFile: (path: string) => void;
-}) {
-  const isEmpty = directories.length === 0 && files.length === 0;
-
-  if (isEmpty) {
-    return (
-      <div className="file-list-container">
-        <div className="empty-state">This directory is empty.</div>
-      </div>
-    );
-  }
-
-  return (
-    <div className="file-list-container">
-       {/* Optional header row like GitHub */}
-      <div className="file-header">
-        Files
-      </div>
-
-      <div id="fileListBody">
-        {directories.map((dir) => (
-          <FileRow
-            key={dir.abspath}
-            item={dir}
-            iconUrl={ICONS.folder}
-            isDir={true}
-            onNavigate={onNavigate}
-            onOpenFile={onOpenFile}
-          />
-        ))}
-
-        {files.map((file) => (
-          <FileRow
-            key={file.abspath}
-            item={file}
-            iconUrl={ICONS.file}
-            isDir={false}
-            onNavigate={onNavigate}
-            onOpenFile={onOpenFile}
-          />
-        ))}
-      </div>
-    </div>
-  );
-}
-
-/**
- * Component: FileRow
- */
-function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: {
-  item: { abspath: string; basename: string };
-  iconUrl: string;
-  isDir: boolean;
-  onNavigate: (path: string) => void;
-  onOpenFile: (path: string) => void;
-}) {
-  const handleClick = (e: React.MouseEvent) => {
-    e.preventDefault();
-    if (isDir) {
-      onNavigate(item.abspath);
-    } else if (isCodeFile(item.basename)) {
-      onOpenFile(item.abspath);
-    } else {
-      // For non-code files, open in new tab
-      window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
-    }
-  };
-
-  const handleMouseEnter = () => {
-    // Prefetch on hover
-    if (isDir) {
-      prefetchDirectory(item.abspath);
-    } else if (isCodeFile(item.basename)) {
-      prefetchFile(item.abspath);
-    }
-  };
-
-  const href = isDir
-    ? `?path=${encodeURIComponent(item.abspath)}`
-    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
-
-  return (
-    <div className="file-row" onMouseEnter={handleMouseEnter}>
-      <span className="icon">
-        <img src={iconUrl} alt={isDir ? "Directory" : "File"} />
-      </span>
-      <span className="name">
-        <a href={href} onClick={handleClick}>
-          {item.basename}
-        </a>
-      </span>
-    </div>
-  );
-}
-
-/**
- * Component: ReadmeViewer
- */
-function ReadmeViewer({ content }: { content: string | null }) {
-  const contentRef = useRef<HTMLDivElement>(null);
-  const moduleRef = useRef<any>(null);
-  const [wasmReady, setWasmReady] = useState(false);
-
-  useEffect(() => {
-    createMarkdownModule().then((Module: any) => {
-      moduleRef.current = Module;
-      setWasmReady(true);
-    });
-  }, []);
-
-  useEffect(() => {
-    if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return;
-
-    const Module = moduleRef.current;
-    const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
-    const markdownFree = Module.cwrap('markdown_free', null, ['number']);
-
-    const ptr = markdownToHtmlPtr(content);
-    const html = Module.UTF8ToString(ptr);
-    markdownFree(ptr);
-    contentRef.current.innerHTML = html;
-  }, [content, wasmReady]);
-
-  if (!content) return null;
-
-  return (
-    <div id="readmeSection">
-      <div className="readme-header">
-        <img src={ICONS.file} width="16" alt="" style={{opacity:0.5}} />
-        README.md
-      </div>
-      <div id="readmeContent" ref={contentRef}>
-        {!wasmReady && 'Loading...'}
-      </div>
-    </div>
-  );
-}
-
-/**
- * Main Application Component
- */
-function RepoBrowser() {
-  const [currentPath, setCurrentPath] = useState(getCurrentPath());
-  const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] });
-  const [readme, setReadme] = useState<string | null>(null);
-  const [error, setError] = useState<string | null>(null);
-  const [loading, setLoading] = useState(false);
-  const [isDarkMode, setIsDarkMode] = useState(() => {
-    // Check localStorage or system preference
-    const saved = localStorage.getItem('theme');
-    if (saved) return saved === 'dark';
-    return window.matchMedia('(prefers-color-scheme: dark)').matches;
-  });
-  const [viewingFile, setViewingFile] = useState<string | null>(null);
-
-  function getCurrentPath() {
-    const params = new URLSearchParams(window.location.search);
-    return params.get('path') || '';
-  }
-
-  // Persist theme preference
-  useEffect(() => {
-    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
-  }, [isDarkMode]);
-
-  useEffect(() => {
-    const handlePopState = () => setCurrentPath(getCurrentPath());
-    window.addEventListener('popstate', handlePopState);
-    return () => window.removeEventListener('popstate', handlePopState);
-  }, []);
-
-  useEffect(() => {
-    fetchDirectory(currentPath);
-    fetchReadme(currentPath);
-  }, [currentPath]);
-
-  const navigate = (path: string) => {
-    const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/';
-    window.history.pushState({ path }, '', newUrl);
-    setCurrentPath(path);
-  };
-
-  const fetchDirectory = async (path: string) => {
-    setLoading(true);
-    setError(null);
-    try {
-      // Check prefetch cache first
-      const cacheKey = `dir:${path}`;
-      let data;
-      if (prefetchCache.has(cacheKey)) {
-        data = await prefetchCache.get(cacheKey);
-        prefetchCache.delete(cacheKey); // Clear after use for fresh data next time
-      } else {
-        const url = path
-          ? `${API_BASE}/list?path=${encodeURIComponent(path)}`
-          : `${API_BASE}/list`;
-        const response = await fetch(url);
-        if (response.ok) {
-          data = await response.json();
-        }
-      }
-
-      if (data?.error) {
-        throw new Error(data.error);
-      }
-
-      setContent({
-        files: data?.files || [],
-        directories: data?.directories || []
-      });
-    } catch (err: any) {
-      console.error('Error loading directory:', err);
-      setError(err.message);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const fetchReadme = async (path: string) => {
-    setReadme(null);
-    const readmePath = path ? `${path}/README.md` : 'README.md';
-    try {
-      const response = await fetch(`${API_BASE}/file?path=${encodeURIComponent(readmePath)}`);
-      if (response.ok) {
-        const text = await response.text();
-        setReadme(text);
-      }
-    } catch (err) {
-      // Readme is optional, ignore errors
-    }
-  };
-
-  const handleOpenFile = useCallback((path: string) => {
-    setViewingFile(path);
-  }, []);
-
-  const handleCloseFile = useCallback(() => {
-    setViewingFile(null);
-  }, []);
-
-  const toggleTheme = () => {
-    setIsDarkMode(prev => !prev);
-  };
-
-  return (
-    <>
-      <GlobalStyles isDark={isDarkMode} />
-      <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>
-          <button className="theme-toggle" onClick={toggleTheme} title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
-            {isDarkMode ? <SunIcon color="#f0c674" /> : <MoonIcon color="#6e7681" />}
-            {isDarkMode ? 'Light' : 'Dark'}
-          </button>
-        </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>
-
-        {/* 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: 'var(--text-secondary)'}}>
-             Loading files...
-          </div>
-        ) : (
-          <>
-            <FileList
-              directories={content.directories}
-              files={content.files}
-              onNavigate={navigate}
-              onOpenFile={handleOpenFile}
-            />
-            <ReadmeViewer content={readme} />
-          </>
-        )}
-      </div>
-
-      {/* File Viewer Modal */}
-      {viewingFile && (
-        isMarkdownFile(viewingFile) ? (
-          <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
-        ) : (
-          <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
-        )
-      )}
-    </>
-  );
-}
-
-export { RepoBrowser };
--- a/mrjunejune/BUILD	Sat Jan 24 21:52:14 2026 -0800
+++ b/mrjunejune/BUILD	Sun Jan 25 20:04:55 2026 -0800
@@ -30,7 +30,13 @@
 
 filegroup(
   name = "public_files",
-  srcs = glob(["src/public/**"]),
+  srcs = glob(["src/public/*"]),
+  visibility = ["//visibility:public"],
+)
+
+filegroup(
+  name = "public_fonts_files",
+  srcs = glob(["src/public/fonts/*"]),
   visibility = ["//visibility:public"],
 )
     
--- a/react_games/public/base.css	Sat Jan 24 21:52:14 2026 -0800
+++ b/react_games/public/base.css	Sun Jan 25 20:04:55 2026 -0800
@@ -5,7 +5,6 @@
 }
 
 body {
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
   background: linear-gradient(135deg, #667eea 10%, #764ba2 100%);
   background-attachment: fixed;
   background-repeat: no-repeat;