ModsCode / Gallery / Designs / Git graph
Git graph
Commit history on main, fix and feature lanes with curved branch and merge links, short shas, branch tags and HEAD on the tip.


hooks/designs/git-graph.mjs
// git-graph: fourteen commits of a mod repo on three lanes, newest at the top as in git log --graph.
// The lanes run main, fix, feature from the left: with fix in the middle, no branch or merge curve
// crosses a lane that is live in those rows. A commit's first parent keeps its lane and is a straight
// line; a branch bends out just above the commit it starts from and a merge bends in just below the
// merge commit, each a one-row S-curve. Lanes are told apart by ink steps (main 0.9, feature 0.65, fix
// 0.42) on the dots and by the ladder's 0.55, 0.35 and 0.25 on the lines; merge commits are hollow
// rings. Rows are 20px, the sha is in the mono face, branch tags are kanban's outlined tag pill in a
// column before the age, and HEAD on the main tip is the one Claude orange item. Static sample history, so
// every render is the same. Sample data only.
const W = 380
const H = 312
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const CLAUDE = '#D77757'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const MONO = "ui-monospace,'Cascadia Mono',Consolas,monospace"
const f = n => n.toFixed(1)
const ROW0 = 22 // centre of the newest row
const ROW = 20
const LANES = [
{ name: 'main', x: 16, op: 0.9, line: 0.55 },
{ name: 'fix', x: 28, op: 0.42, line: 0.25 },
{ name: 'feature', x: 40, op: 0.65, line: 0.35 },
]
const SHA_X = 56
const SUBJECT_X = 106
const TAG_R = 340 // right edge of the tag column; the age sits right-aligned at W - 12
// Oldest first. lane indexes LANES; parents index this list, the first parent first.
const COMMITS = [
{ sha: '1f3c0a2', lane: 0, parents: [], subject: 'init look mod', age: '9d' },
{ sha: '7b21e94', lane: 0, parents: [0], subject: 'feat: candle design', age: '8d' },
{ sha: 'c40d8a1', lane: 2, parents: [1], subject: 'feat: gallery grid', age: '7d' },
{ sha: '5e9a3f0', lane: 0, parents: [1], subject: 'chore: pin d3 bundle', age: '6d' },
{ sha: '9a07b6c', lane: 2, parents: [2], subject: 'feat: draw designs lazily', age: '6d' },
{ sha: '2d8e41f', lane: 1, parents: [3], subject: 'fix: round token counts', age: '5d' },
{ sha: 'e61f0b3', lane: 0, parents: [3], subject: 'test: check.mjs contract', age: '4d' },
{ sha: '84c2d17', lane: 1, parents: [5], subject: 'fix: guard empty window', age: '4d', tag: 'fix/usage-round' },
{ sha: 'b3a95e0', lane: 0, parents: [6, 7], subject: 'Merge fix/usage-round', age: '3d' },
{ sha: '0f6d2c8', lane: 2, parents: [4, 8], subject: 'Merge main into feat/gallery', age: '2d' },
{ sha: '6ac81e5', lane: 2, parents: [9], subject: 'feat: sheet export at 2x', age: '1d', tag: 'feat/gallery' },
{ sha: 'd27b4f9', lane: 0, parents: [8], subject: 'chore: bump 0.4.1', age: '20h' },
{ sha: '49e0c7a', lane: 0, parents: [11, 10], subject: 'Merge feat/gallery', age: '6h' },
{ sha: 'a3f9c21', lane: 0, parents: [12], subject: 'release: 0.5.0', age: '2h', tag: 'main', head: true },
]
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start', font = FONT } = {}) =>
`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${font}" fill="${fill}">${s}</text>`
// kanban's tag pill, right-aligned at x. The font is not measured, so widths at 9px come from three
// classes read off a Segoe UI render: a capital 5.9px, m or w 7.4px, the narrow f i j l r t and / - . 2.6px,
// any other character 4.9px. One flat width per character left feat/gallery with 14px of air on its right
// and 4px on its left; the classes keep both sides near the 4px of padding
const pillW = label => 8 + [...label].reduce((w, ch) => w + (/[A-Z]/.test(ch) ? 5.9 : /[mw]/.test(ch) ? 7.4 : /[fijlrt/.-]/.test(ch) ? 2.6 : 4.9), 0)
function tagPill(right, cy, label, colour, op) {
const w = Math.round(pillW(label))
const x = right - w
return {
w,
svg: `<rect x="${f(x + 0.5)}" y="${f(cy - 6.5)}" width="${w}" height="13" rx="2" fill="none" stroke="${colour}" stroke-opacity="${op}"/>` +
text(x + 4.5, cy + 3, label, { fill: colour }),
}
}
function draw() {
const n = COMMITS.length
const rowY = i => ROW0 + (n - 1 - i) * ROW // newest at the top
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
// Links, child to parent, before the dots
COMMITS.forEach((c, i) => {
c.parents.forEach((p, k) => {
const a = LANES[c.lane]
const b = LANES[COMMITS[p].lane]
const yc = rowY(i)
const yp = rowY(p)
let d
let lane
if (a === b) {
d = `M${a.x} ${f(yc)}V${f(yp)}`
lane = a
} else if (k === 0) {
// A branch: run down the child's lane, then bend into the parent in the last row
const ys = yp - ROW
d = `M${a.x} ${f(yc)}V${f(ys)}C${a.x} ${f(ys + ROW / 2)} ${b.x} ${f(ys + ROW / 2)} ${b.x} ${f(yp)}`
lane = a
} else {
// A merge: bend out of the merge commit in the first row, then run down the parent's lane
const ye = yc + ROW
d = `M${a.x} ${f(yc)}C${a.x} ${f(yc + ROW / 2)} ${b.x} ${f(yc + ROW / 2)} ${b.x} ${f(ye)}V${f(yp)}`
lane = b
}
parts.push(`<path d="${d}" fill="none" stroke="${INK}" stroke-opacity="${lane.line}"/>`)
})
})
COMMITS.forEach((c, i) => {
const y = rowY(i)
const lane = LANES[c.lane]
if (c.parents.length > 1) {
parts.push(`<circle cx="${lane.x}" cy="${f(y)}" r="2.5" fill="${STAGE}" stroke="${INK}" stroke-opacity="${lane.op}"/>`)
} else {
parts.push(`<circle cx="${lane.x}" cy="${f(y)}" r="2" fill="${INK}" fill-opacity="${lane.op}"/>`)
}
parts.push(text(SHA_X, y + 3.5, c.sha, { size: 10, font: MONO }))
parts.push(text(SUBJECT_X, y + 3.5, c.subject, { size: 10, fill: INK }))
// Tags line up in a column that ends before the age, so their place does not hang on an estimate
// of the subject's width; HEAD stands to the left of the branch it points at
let tx = TAG_R
if (c.tag) {
const p = tagPill(tx, y, c.tag, MUTED, 0.5)
parts.push(p.svg)
tx -= p.w + 4
}
if (c.head) parts.push(tagPill(tx, y, 'HEAD', CLAUDE, 0.9).svg)
parts.push(text(W - 12, y + 3, c.age, { anchor: 'end' }))
})
parts.push(text(12, H - 8, `SAMPLE ${COMMITS.length} COMMITS`))
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}
let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
id: 'git-graph',
title: 'Git graph',
category: 'code',
description: 'Commit history on main, fix and feature lanes with curved branch and merge links, short shas, branch tags and HEAD on the tip.',
element: ui => ui.Svg({ source: (source ??= draw()), alt: `Git graph of ${COMMITS.length} commits on ${LANES.length} lanes with branch and merge curves and HEAD on main, sample data`, width: W, height: H }),
}
To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.
Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).