diff hg-web/src/components/graph.tsx @ 221:ce7f4400c2de hg-web

[hg-web] Harden forge and add changeset UI
author MrJuneJune <me@mrjunejune.com>
date Sun, 02 Aug 2026 09:01:24 -0700
parents fb28063dc490
children 3007ef5fc0ed
line wrap: on
line diff
--- a/hg-web/src/components/graph.tsx	Sun Aug 02 08:34:54 2026 -0700
+++ b/hg-web/src/components/graph.tsx	Sun Aug 02 09:01:24 2026 -0700
@@ -177,17 +177,11 @@
 const Graph = ({ data, loading, hasMore, onLoadMore, onCommitClick, maxRows }: GraphProps) => {
   const canvasRef = useRef<HTMLCanvasElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
+  const [assetError, setAssetError] = useState<string | null>(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_lines.png";
-
-  const pandaImg = new Image();
-  pandaImg.src = "http://localhost:6970/panda.png";
-
   useEffect(() => {
     const canvas = canvasRef.current;
     if (!canvas || !changesets.length) return;
@@ -195,20 +189,11 @@
     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();
+    let cancelled = false;
+    let pencilPattern: CanvasPattern | null = null;
+    let loadedAssets = 0;
+    const pencilImage = new Image();
+    const pandaImage = new Image();
     const dpr = window.devicePixelRatio || 1;
     const maxCol = Math.max(...changesets.map(cs => cs.col), 0);
     const canvasWidth = (maxCol + 2) * colWidth;
@@ -241,13 +226,35 @@
       // Pass 2: Draw Commit Nodes
       changesets.forEach((cs, i) => {
         const x = getX(cs.col), y = getY(i);
-        ctx.drawImage(pandaImg, x-10, y-10, 20, 20);
+        ctx.drawImage(pandaImage, x-10, y-10, 20, 20);
       });
     };
 
-    img.onload = () => {
-      pencilPattern = ctx.createPattern(img, "repeat")!;
-      renderCanvas(); 
+    const handleAssetLoad = () => {
+      loadedAssets++;
+      if (loadedAssets !== 2 || cancelled) return;
+      pencilPattern = ctx.createPattern(pencilImage, "repeat");
+      renderCanvas();
+    };
+
+    const handleAssetError = () => {
+      if (!cancelled) setAssetError('Unable to load the graph artwork.');
+    };
+
+    setAssetError(null);
+    pencilImage.onload = handleAssetLoad;
+    pencilImage.onerror = handleAssetError;
+    pandaImage.onload = handleAssetLoad;
+    pandaImage.onerror = handleAssetError;
+    pencilImage.src = "/pencil_lines.png";
+    pandaImage.src = "/panda.png";
+
+    return () => {
+      cancelled = true;
+      pencilImage.onload = null;
+      pencilImage.onerror = null;
+      pandaImage.onload = null;
+      pandaImage.onerror = null;
     };
   }, [changesets]);
 
@@ -266,45 +273,38 @@
   }, [onLoadMore, hasMore, loading]);
 
   return (
-    <div style={{ display: 'flex', flexDirection: 'column', height: '100%', backgroundImage: 'url("/hg-web-background.jpg")', fontFamily: 'monospace' }}>
+    <div className="graph-container" style={{ backgroundImage: 'url("/hg-web-background.jpg")' }}>
+      {assetError && <div className="error-message">{assetError}</div>}
       <div 
         ref={containerRef} 
-        style={{ display: 'flex', flex: 1, overflowY: 'auto', position: 'relative' }}
+        className="graph-wrapper"
       >
-        {/* 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' }}>
+        <div className="graph-canvas-column">
           <canvas ref={canvasRef} style={{ display: 'block' }} />
         </div>
 
-        {/* Details Column */}
-        <div style={{ flex: 1 }}>
+        <div className="graph-details-column">
           {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'
-              }}
+            <button
+              type="button"
+              key={cs.node}
+              className="graph-row"
               onClick={() => onCommitClick?.(cs.node)}
-              onMouseEnter={(e) => (e.currentTarget.style.background = '#222')}
-              onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
+              aria-label={`Open changeset ${cs.node.substring(0, 12)}: ${cs.desc}`}
             >
-              <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>
+              <span className="graph-row-meta">
+                <span className="graph-hash">{cs.node.substring(0, 12)}</span>
+                <span className="graph-user">{cs.user.split(' <')[0]}</span>
+                {cs.branch && <span className="graph-branch">{cs.branch}</span>}
+              </span>
+              <span className="graph-desc">{cs.desc}</span>
+            </button>
           ))}
           <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>}
+      {loading && <div className="graph-loading-row">Loading repository history...</div>}
     </div>
   );
 };