Mercurial
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> ); };