Git Explorer
muse spark 1.2 · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 55,088 bytes · SHA-256 62b73af1cfd9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Git Topology — Structure Visualizer</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0a0e14;
--panel:#111720;
--panel2:#151d2b;
--border:#1e2a3d;
--border2:#24344f;
--text:#e6edf3;
--muted:#8b9bb4;
--dim:#5a6d8a;
--accent:#2f81f7;
--violet:#a371f7;
--green:#3fb950;
--orange:#f0883e;
--pink:#db61a2;
--yellow:#d29922;
--red:#f85149;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:'Inter',system-ui,sans-serif;overflow:hidden}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:#24344f;border-radius:999px}
::-webkit-scrollbar-track{background:transparent}
button,input,select,textarea{font:inherit}
button{cursor:pointer}
/* header */
.header{
height:56px;display:flex;align-items:center;justify-content:space-between;
padding:0 16px;border-bottom:1px solid var(--border);background:#0d1117;position:sticky;top:0;z-index:40;
}
.brand{display:flex;align-items:center;gap:12px}
.logo{
width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#2f81f7,#a371f7);
display:grid;place-items:center;font-family:'JetBrains Mono',monospace;font-weight:800;font-size:14px;box-shadow:0 6px 20px rgba(47,129,247,.35)
}
.brand h1{font-size:15px;font-weight:800;letter-spacing:-.02em;margin:0;line-height:1}
.brand p{margin:0;color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase;font-weight:600}
.stats{display:flex;gap:8px;align-items:center}
.stat{
background:var(--panel);border:1px solid var(--border);border-radius:999px;padding:6px 10px;
display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;letter-spacing:.02em
}
.stat b{color:var(--text)} .stat span{color:var(--muted);font-weight:600}
.dot{width:8px;height:8px;border-radius:50%}
.actions{display:flex;gap:8px;align-items:center}
.btn{
border:1px solid var(--border);background:var(--panel);color:var(--text);
padding:7px 12px;border-radius:8px;font-size:12px;font-weight:600;display:inline-flex;align-items:center;gap:6px;transition:.15s
}
.btn:hover{border-color:var(--border2);transform:translateY(-1px)}
.btn-primary{background:var(--accent);border-color:var(--accent);color:white;box-shadow:0 6px 16px rgba(47,129,247,.35)}
.btn-ghost{background:transparent}
.btn-small{padding:5px 9px;font-size:11px;border-radius:7px}
.icon{width:14px;height:14px;opacity:.9}
/* layout */
.app{display:grid;grid-template-columns:300px 1fr 360px;height:calc(100vh - 56px)}
@media(max-width:1200px){.app{grid-template-columns:280px 1fr}.right{ display:none } }
@media(max-width:860px){.app{grid-template-columns:1fr;height:auto;overflow:auto}body{overflow:auto}.header{position:sticky}.graph-wrap{height:520px}}
.left,.right{background:var(--panel);border-right:1px solid var(--border);overflow:auto}
.right{border-right:none;border-left:1px solid var(--border);background:#0f141e}
.panel{padding:14px;border-bottom:1px solid var(--border)}
.panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.panel-head h3{margin:0;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:800;display:flex;align-items:center;gap:8px}
.badge{font-size:10px;font-weight:800;background:#1a2335;border:1px solid var(--border2);color:var(--muted);padding:2px 6px;border-radius:999px}
.card{background:var(--panel2);border:1px solid var(--border);border-radius:12px;padding:10px}
.input, .select, .textarea{
width:100%;background:#0d1117;border:1px solid var(--border);color:var(--text);
border-radius:8px;padding:8px 9px;font-size:12px;outline:none
}
.input:focus,.select:focus,.textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(47,129,247,.15)}
.textarea{min-height:56px;resize:vertical}
.label{font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:8px 0 6px;display:block}
.row{display:flex;gap:8px}
.row>*{flex:1}
.mini{font-size:11px;color:var(--muted);line-height:1.4}
.link{color:var(--accent);text-decoration:none;font-weight:600}
.list{display:flex;flex-direction:column;gap:8px;max-height:220px;overflow:auto;padding-right:2px}
.item{
background:#0d1117;border:1px solid var(--border);border-radius:10px;padding:8px 9px;
display:flex;gap:8px;align-items:flex-start;transition:.15s
}
.item:hover{border-color:var(--border2)}
.item.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(47,129,247,.18)}
.item-title{font-size:12px;font-weight:700;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.item-sub{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.avatar{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:800;color:white;flex:0 0 22px}
.branch-row{display:flex;align-items:center;gap:8px;padding:8px;border-radius:10px;border:1px solid var(--border);background:#0d1117}
.branch-name{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700}
.branch-meta{font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace}
.pill{font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;padding:3px 7px;border-radius:999px;border:1px solid var(--border);background:var(--panel2)}
.pill-main{color:#3fb950;border-color:rgba(63,185,80,.3);background:rgba(63,185,80,.12)}
.pill-feat{color:#a371f7;border-color:rgba(163,113,247,.3);background:rgba(163,113,247,.12)}
.pill-hot{color:#f85149;border-color:rgba(248,81,73,.3);background:rgba(248,81,73,.12)}
/* center */
.center{display:flex;flex-direction:column;min-width:0;background:radial-gradient(1200px 600px at 30% -10%, rgba(47,129,247,.12), transparent 60%), radial-gradient(900px 500px at 90% 0%, rgba(163,113,247,.10), transparent 60%), var(--bg)}
.toolbar{
display:flex;align-items:center;justify-content:space-between;gap:10px;
padding:10px 12px;border-bottom:1px solid var(--border);background:rgba(13,17,23,.85);backdrop-filter:blur(8px);position:sticky;top:0;z-index:5
}
.tabs{display:flex;background:#0d1117;border:1px solid var(--border);border-radius:999px;padding:3px;gap:3px}
.tab{padding:6px 12px;border-radius:999px;font-size:12px;font-weight:700;color:var(--muted);border:none;background:transparent}
.tab.active{background:var(--text);color:#0a0e14}
.graph-wrap{position:relative;flex:1;overflow:hidden;min-height:0}
#graph{width:100%;height:100%;display:block;background:
linear-gradient(rgba(30,42,61,.35) 1px, transparent 1px),
linear-gradient(90deg, rgba(30,42,61,.35) 1px, transparent 1px);
background-size:28px 28px; background-position: -1px -1px;
}
.graph-controls{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:8px;z-index:6}
.ctrl{width:36px;height:36px;border-radius:10px;border:1px solid var(--border);background:var(--panel);display:grid;place-items:center;color:var(--text);box-shadow:0 8px 20px rgba(0,0,0,.35)}
.legend{position:absolute;left:12px;bottom:12px;display:flex;gap:8px;align-items:center;background:rgba(17,23,32,.9);border:1px solid var(--border);border-radius:999px;padding:6px 10px;backdrop-filter:blur(6px);z-index:6}
.legend i{width:10px;height:10px;border-radius:50%;display:inline-block}
.tooltip{
position:absolute;pointer-events:none;z-index:20;min-width:260px;max-width:340px;
background:#111720;border:1px solid var(--border2);border-radius:12px;padding:12px;
box-shadow:0 16px 40px rgba(0,0,0,.45);transform:translate(-50%, -100%);opacity:0;transition:.12s
}
.tooltip.show{opacity:1}
.tooltip h4{margin:0 0 4px;font-size:12px;font-weight:800}
.tooltip p{margin:0;color:var(--muted);font-size:11px;line-height:1.5}
.tag{font-size:10px;font-weight:800;padding:3px 6px;border-radius:999px;border:1px solid var(--border);background:#0d1117;color:var(--muted)}
/* right */
.detail-card{background:linear-gradient(180deg, #151d2b, #111720);border:1px solid var(--border);border-radius:14px;padding:12px}
.commit-big{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);font-weight:700}
.timeline{position:relative;padding-left:18px}
.timeline::before{content:"";position:absolute;left:6px;top:6px;bottom:6px;width:2px;background:linear-gradient(var(--accent), var(--violet));border-radius:999px;opacity:.35}
.evt{position:relative;background:#0d1117;border:1px solid var(--border);border-radius:12px;padding:10px;margin-bottom:10px;transition:.15s}
.evt:hover{border-color:var(--border2);transform:translateY(-1px)}
.evt.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(47,129,247,.18)}
.evt::before{content:"";position:absolute;left:-14px;top:14px;width:10px;height:10px;border-radius:50%;border:2px solid var(--bg);background:var(--accent);box-shadow:0 0 0 3px rgba(47,129,247,.25)}
.evt-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
.evt-title{font-size:12px;font-weight:800}
.evt-desc{font-size:11px;color:var(--muted);line-height:1.45}
.mono{font-family:'JetBrains Mono',monospace}
.kbd{font-family:'JetBrains Mono',monospace;font-size:10px;background:#0d1117;border:1px solid var(--border);border-bottom-width:2px;padding:2px 5px;border-radius:6px;color:var(--muted)}
.divider{height:1px;background:var(--border);margin:10px 0}
.empty{padding:14px;text-align:center;color:var(--muted);font-size:12px;border:1px dashed var(--border);border-radius:12px;background:rgba(255,255,255,.02)}
</style>
</head>
<body>
<header class="header">
<div class="brand">
<div class="logo">◈</div>
<div>
<h1>Git Topology <span style="font-weight:400;color:var(--muted)">— Structure Visualizer</span></h1>
<p>dag synthesis • anchored context • live DAG</p>
</div>
</div>
<div class="stats" id="stats"></div>
<div class="actions">
<label class="btn" for="importFile">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Import
</label>
<input id="importFile" type="file" accept=".json" hidden>
<button class="btn" id="exportBtn">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export
</button>
<button class="btn" id="resetBtn">Reset Demo</button>
<button class="btn btn-primary" id="newCommitBtn">+ New Commit</button>
</div>
</header>
<div class="app">
<!-- LEFT -->
<aside class="left">
<div class="panel">
<div class="panel-head"><h3>⚡ Branches <span class="badge" id="branchCount">0</span></h3><button class="btn btn-small" id="addBranchBtn">+ Branch</button></div>
<div id="branchList" style="display:flex;flex-direction:column;gap:8px"></div>
<div class="card" style="margin-top:10px">
<label class="label">Create Branch</label>
<input class="input" id="newBranchName" placeholder="feature/payments">
<label class="label">From commit</label>
<select class="select" id="newBranchCommit"></select>
<div style="margin-top:8px" class="row">
<button class="btn btn-primary" id="createBranchBtn" style="flex:1">Create</button>
<button class="btn" id="clearBranchBtn">Clear</button>
</div>
</div>
</div>
<div class="panel">
<div class="panel-head"><h3>⬢ Commits <span class="badge" id="commitCount">0</span></h3><button class="btn btn-small" id="focusGraphBtn">Fit</button></div>
<div class="card">
<label class="label">Message</label>
<input class="input" id="cMessage" placeholder="feat: add checkout flow">
<div class="row">
<div><label class="label">Author</label><input class="input" id="cAuthor" placeholder="you"></div>
<div><label class="label">Branch</label><select class="select" id="cBranch"></select></div>
</div>
<label class="label">Parents (hold Ctrl/Cmd for merge)</label>
<select class="select" id="cParents" multiple size="3" style="height:74px"></select>
<div class="mini" style="margin-top:6px">Create then drag on graph. Delete blocked if commit has children.</div>
<div class="row" style="margin-top:8px">
<button class="btn btn-primary" id="createCommitBtn">Commit</button>
<button class="btn" id="clearCommitBtn">Reset</button>
</div>
<div class="row" style="margin-top:8px">
<button class="btn" id="editCommitBtn" style="flex:1;display:none">Save Edit</button>
<button class="btn" id="cancelEditBtn" style="display:none">Cancel</button>
</div>
</div>
<div class="label">History — click to select</div>
<div class="list" id="commitList"></div>
</div>
<div class="panel">
<div class="panel-head"><h3>◆ Anchored Events <span class="badge" id="eventCount">0</span></h3></div>
<div class="card">
<label class="label">Title</label>
<input class="input" id="eTitle" placeholder="Code Freeze">
<div class="row">
<div><label class="label">Type</label>
<select class="select" id="eType"><option>Milestone</option><option>Code Freeze</option><option>Review</option><option>Deploy</option><option>Incident</option><option>Feature Complete</option></select>
</div>
<div><label class="label">Anchored Commit *</label><select class="select" id="eCommit"></select></div>
</div>
<label class="label">Description</label>
<textarea class="textarea" id="eDesc" placeholder="Context for this point in history..."></textarea>
<div class="row" style="margin-top:8px">
<button class="btn btn-primary" id="createEventBtn">Anchor Event</button>
<button class="btn" id="clearEventBtn">Clear</button>
</div>
<div class="row" style="margin-top:8px">
<button class="btn" id="saveEventEditBtn" style="display:none;flex:1">Save Event</button>
<button class="btn" id="cancelEventEditBtn" style="display:none">Cancel</button>
</div>
</div>
</div>
</aside>
<!-- CENTER -->
<main class="center">
<div class="toolbar">
<div style="display:flex;gap:10px;align-items:center">
<div class="tabs">
<button class="tab active" data-tab="graph">Graph</button>
<button class="tab" data-tab="context">Context Flow</button>
</div>
<span class="mini" style="display:flex;gap:6px;align-items:center"><span class="kbd">Click</span> select • <span class="kbd">Hover flag</span> details • <span class="kbd">Scroll</span> zoom</span>
</div>
<div style="display:flex;gap:8px;align-items:center">
<span class="mini" id="zoomLabel">100%</span>
<button class="btn btn-small" id="zoomIn">+</button>
<button class="btn btn-small" id="zoomOut">−</button>
<button class="btn btn-small" id="fitBtn">Fit View</button>
</div>
</div>
<div class="graph-wrap" id="graphWrap">
<svg id="graph"></svg>
<div class="graph-controls">
<button class="ctrl" id="cZoomIn" title="Zoom in">+</button>
<button class="ctrl" id="cZoomOut" title="Zoom out">-</button>
<button class="ctrl" id="cFit" title="Fit">⧉</button>
</div>
<div class="legend">
<span style="display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700"><i style="background:#3fb950"></i> main</span>
<span style="display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700"><i style="background:#a371f7"></i> feature</span>
<span style="display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700"><i style="background:#f0883e"></i> hotfix</span>
<span style="display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700"><i style="background:#f85149"></i> anchored</span>
</div>
<div class="tooltip" id="tooltip"></div>
</div>
<div id="contextView" style="display:none;flex:1;overflow:auto;padding:16px;background:var(--bg)">
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px">
<div class="card" style="flex:1;min-width:220px">
<div style="font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)">Contextual Flow Display</div>
<div style="font-size:13px;font-weight:700;margin-top:4px">All external markers ordered by DAG position</div>
<div class="mini" style="margin-top:4px">Events are visually anchored to commit nodes in the graph. Hover flags to confirm association.</div>
</div>
<div class="card" style="width:280px">
<div class="mini">Filter</div>
<select class="select" id="filterType"><option value="all">All types</option><option>Milestone</option><option>Code Freeze</option><option>Review</option><option>Deploy</option><option>Incident</option><option>Feature Complete</option></select>
</div>
</div>
<div id="contextGrid" style="display:grid;grid-template-columns:1fr 1fr;gap:12px"></div>
</div>
</main>
<!-- RIGHT -->
<aside class="right">
<div class="panel">
<div class="panel-head"><h3>◎ Selected</h3><span class="kbd" id="selectedId">none</span></div>
<div id="selectedPanel"></div>
</div>
<div class="panel">
<div class="panel-head"><h3>◐ Contextual Flow</h3><span class="badge" id="flowCount">0 events</span></div>
<div class="timeline" id="flowList"></div>
</div>
<div class="panel">
<div class="panel-head"><h3>▣ Tags</h3><button class="btn btn-small" id="addTagBtn">+ Tag</button></div>
<div id="tagList" style="display:flex;flex-direction:column;gap:8px"></div>
<div class="card" style="margin-top:10px">
<div class="row">
<input class="input" id="tagName" placeholder="v1.2.0">
<select class="select" id="tagCommit"></select>
</div>
<button class="btn btn-primary" style="width:100%;margin-top:8px" id="createTagBtn">Create Tag</button>
</div>
<div class="mini" style="margin-top:10px">Tip: anchored events show as flags on nodes. Selecting a commit highlights its event. Try importing JSON.</div>
</div>
</aside>
</div>
<script>
const STORAGE_KEY = 'gitVisualizerState_v3';
const BRANCH_COLORS = { main:'#3fb950', develop:'#2f81f7', 'feature/auth':'#a371f7', 'feature/payments':'#db61a2', 'hotfix/urgent':'#f85149', 'feature/search':'#f0883e' };
const TYPE_COLOR = { 'Milestone':'#2f81f7','Code Freeze':'#54aeff','Review':'#a371f7','Deploy':'#3fb950','Incident':'#f85149','Feature Complete':'#f0883e' };
let state = null;
let selectedCommitId = null;
let editingCommitId = null;
let editingEventId = null;
let zoomTransform = d3.zoomIdentity;
function uid(prefix='c'){ return prefix + Math.random().toString(36).slice(2,8) + Date.now().toString(36).slice(-3); }
function colorForBranch(b){ return BRANCH_COLORS[b] || '#8b9bb4'; }
function seed(){
const commits = [
{id:'c_a1', message:'initial commit — scaffold', author:'alex', date:'2024-01-08T10:00:00Z', parents:[], branch:'main'},
{id:'c_b2', message:'feat: add auth service & JWT', author:'maria', date:'2024-01-12T10:00:00Z', parents:['c_a1'], branch:'main'},
{id:'c_c3', message:'chore: setup eslint & ci', author:'alex', date:'2024-01-14T10:00:00Z', parents:['c_b2'], branch:'main'},
{id:'c_d4', message:'feature/auth — oauth provider', author:'maria', date:'2024-01-16T11:00:00Z', parents:['c_b2'], branch:'feature/auth'},
{id:'c_e5', message:'feat: payments stub', author:'sam', date:'2024-01-18T10:00:00Z', parents:['c_c3'], branch:'main'},
{id:'c_f6', message:'feature/auth — sessions & refresh', author:'maria', date:'2024-01-20T10:00:00Z', parents:['c_d4'], branch:'feature/auth'},
{id:'c_g7', message:'hotfix: patch token expiry', author:'alex', date:'2024-01-21T09:00:00Z', parents:['c_c3'], branch:'hotfix/urgent'},
{id:'c_h8', message:'Merge feature/auth into main', author:'alex', date:'2024-01-23T10:00:00Z', parents:['c_e5','c_f6'], branch:'main'},
{id:'c_i9', message:'feat: search indexing', author:'sam', date:'2024-01-25T10:00:00Z', parents:['c_h8'], branch:'feature/search'},
{id:'c_j10', message:'Merge hotfix/urgent into main', author:'alex', date:'2024-01-26T10:00:00Z', parents:['c_h8','c_g7'], branch:'main'},
{id:'c_k11', message:'feat: cart & checkout', author:'sam', date:'2024-01-29T10:00:00Z', parents:['c_j10'], branch:'main'},
{id:'c_l12', message:'feature/search — fuzzy match', author:'sam', date:'2024-01-30T10:00:00Z', parents:['c_i9'], branch:'feature/search'},
{id:'c_m13', message:'Merge feature/search', author:'alex', date:'2024-02-02T10:00:00Z', parents:['c_k11','c_l12'], branch:'main'},
];
const branches = [
{name:'main', target:'c_m13', color:BRANCH_COLORS.main},
{name:'feature/auth', target:'c_f6', color:BRANCH_COLORS['feature/auth']},
{name:'feature/search', target:'c_l12', color:BRANCH_COLORS['feature/search']},
{name:'hotfix/urgent', target:'c_g7', color:'#f85149'},
];
const events = [
{id:'e1', title:'Project Kickoff', type:'Milestone', commitId:'c_a1', desc:'Repo initialized, architecture decision records created.'},
{id:'e2', title:'Code Freeze — Auth', type:'Code Freeze', commitId:'c_f6', desc:'Auth flow frozen for QA. No further changes without approval.'},
{id:'e3', title:'Security Review', type:'Review', commitId:'c_h8', desc:'Merged auth into main — security review passed.'},
{id:'e4', title:'Hotfix Incident #412', type:'Incident', commitId:'c_g7', desc:'Token expiry caused 401s in prod. Patched and merged.'},
{id:'e5', title:'Feature Complete — Search', type:'Feature Complete', commitId:'c_l12', desc:'Fuzzy search completed with indexing benchmarks.'},
{id:'e6', title:'Release v1.0', type:'Deploy', commitId:'c_m13', desc:'Tagged v1.0 — deployed to production.'},
];
const tags = [{name:'v1.0', commitId:'c_m13', color:'#d29922'}];
return {commits, branches, events, tags};
}
function load(){
try{
const raw = localStorage.getItem(STORAGE_KEY);
if(raw){ const p = JSON.parse(raw); if(p.commits) return p; }
}catch(e){}
return seed();
}
function save(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
function topologicalOrder(commits){
// simple: commits already in seed order; for dynamic we sort by insertion but ensure parents before children
const map = new Map(commits.map(c=>[c.id,c]));
const visited = new Set();
const order=[];
function visit(id){
if(visited.has(id)) return;
visited.add(id);
const c = map.get(id);
if(!c) return;
c.parents.forEach(p=>visit(p));
order.push(c);
}
commits.forEach(c=>visit(c.id));
return order;
}
function computeLayout(){
const ordered = topologicalOrder(state.commits);
// lanes: by branch
const branchList = [...new Set(ordered.map(c=>c.branch))];
// stable order: main first
branchList.sort((a,b)=> (a==='main'?-1:b==='main'?1:a.localeCompare(b)));
const lane = new Map(branchList.map((b,i)=>[b,i]));
const pos = new Map();
const xStep = 128, yStep=72, baseY=46, baseX=60;
ordered.forEach((c, idx)=>{
const l = lane.get(c.branch) ?? 0;
// jitter merges slightly to avoid overlap
pos.set(c.id, {x: baseX + idx * xStep, y: baseY + l * yStep, lane:l, idx});
});
return {ordered, pos, lane, branchList};
}
// D3 rendering
let svg, g, zoom;
function initGraph(){
svg = d3.select('#graph');
g = svg.append('g').attr('id','viewport');
// zoom
zoom = d3.zoom().scaleExtent([0.35, 2.5]).on('zoom', (e)=>{
zoomTransform = e.transform;
g.attr('transform', e.transform);
document.getElementById('zoomLabel').textContent = Math.round(e.transform.k*100)+'%';
});
svg.call(zoom);
svg.on('dblclick.zoom', null);
}
function pathForLink(s, t){
const dx = t.x - s.x;
const mx = s.x + dx*0.5;
// smooth cubic
return `M ${s.x} ${s.y} C ${mx} ${s.y}, ${mx} ${t.y}, ${t.x} ${t.y}`;
}
function renderGraph(){
const {ordered, pos} = computeLayout();
const wrap = document.getElementById('graphWrap');
const w = wrap.clientWidth, h = wrap.clientHeight;
// svg size
const maxX = Math.max(...[...pos.values()].map(p=>p.x), 800) + 160;
const maxY = Math.max(...[...pos.values()].map(p=>p.y), 300) + 80;
svg.attr('viewBox', `0 0 ${maxX} ${h < 420? 520: maxY}`);
// data
const links = [];
state.commits.forEach(c=>{
c.parents.forEach(pid=>{
const s = pos.get(pid), t = pos.get(c.id);
if(s && t) links.push({source:pid, target:c.id, s, t, branch:c.branch});
});
});
// LINKS
const linkSel = g.selectAll('path.link').data(links, d=>d.source+'->'+d.target);
linkSel.enter().append('path').attr('class','link')
.merge(linkSel)
.attr('d', d=> pathForLink(d.s, d.t))
.attr('fill','none')
.attr('stroke', d=> d.branch==='main' ? '#3fb950' : colorForBranch(d.branch))
.attr('stroke-width',2.2)
.attr('opacity',0.95)
.attr('stroke-linecap','round');
linkSel.exit().remove();
// NODES GROUP
const nodeSel = g.selectAll('g.node').data(ordered, d=>d.id);
const nodeEnter = nodeSel.enter().append('g').attr('class','node').style('cursor','pointer');
nodeEnter.append('circle').attr('class','halo');
nodeEnter.append('circle').attr('class','core');
nodeEnter.append('text').attr('class','nlabel');
nodeEnter.append('g').attr('class','flag');
const allNodes = nodeEnter.merge(nodeSel);
allNodes.attr('transform', d=> `translate(${pos.get(d.id).x},${pos.get(d.id).y})`)
.on('click', (e,d)=>{ selectedCommitId = d.id; renderAll(); })
.on('mouseenter', function(e,d){
const p = pos.get(d.id);
const ct = this.getBoundingClientRect();
// highlight
d3.select(this).select('circle.core').attr('stroke-width',3.5);
})
.on('mouseleave', function(e,d){
d3.select(this).select('circle.core').attr('stroke-width', isSelected(d.id)?3:2);
hideTooltip();
});
allNodes.select('circle.halo')
.attr('r', d=> isSelected(d.id)? 28: 22)
.attr('fill', d=> colorForBranch(d.branch))
.attr('opacity', d=> isSelected(d.id)? 0.18: 0)
;
allNodes.select('circle.core')
.attr('r', d=> d.parents.length>1? 16:13)
.attr('fill', d=> d.parents.length>1? '#0d1117' : colorForBranch(d.branch))
.attr('stroke', d=> colorForBranch(d.branch))
.attr('stroke-width', d=> isSelected(d.id)?3:2)
.attr('stroke-opacity',1);
// inner dot for merges
allNodes.select('text.nlabel')
.attr('text-anchor','middle').attr('dy','0.36em')
.attr('font-family','JetBrains Mono, monospace').attr('font-size','9px').attr('font-weight','800')
.attr('fill', d=> d.parents.length>1? colorForBranch(d.branch): '#0a0e14')
.text(d=> d.parents.length>1? '◈' : '●')
.attr('pointer-events','none');
// anchored events flags
allNodes.select('g.flag').each(function(d){
const cont = d3.select(this);
cont.selectAll('*').remove();
const evts = state.events.filter(e=>e.commitId===d.id);
if(evts.length===0) return;
const gFlag = cont.append('g').attr('transform','translate(18, -34)');
// pin line
gFlag.append('line').attr('x1',0).attr('y1',12).attr('x2',0).attr('y2',22).attr('stroke', '#8b9bb4').attr('stroke-width',1.4).attr('opacity',0.9);
// stacked flags if multiple
evts.forEach((ev,i)=>{
const y = i*18;
const fg = gFlag.append('g').attr('transform',`translate(0, ${y})`).style('cursor','pointer');
const w = 92, h=16;
fg.append('rect').attr('x',2).attr('y',0).attr('rx',6).attr('ry',6).attr('width',w).attr('height',h)
.attr('fill', TYPE_COLOR[ev.type] || '#2f81f7')
.attr('stroke','rgba(0,0,0,.25)').attr('stroke-width',1);
fg.append('circle').attr('cx',8).attr('cy',8).attr('r',5).attr('fill','white').attr('opacity',0.95);
fg.append('text').attr('x',8).attr('y',8).attr('dy','0.36em').attr('text-anchor','middle').attr('font-size','7px').attr('font-weight','900').attr('fill','#0a0e14').text('✦');
fg.append('text').attr('x',16).attr('y',8).attr('dy','0.36em').attr('font-size','8px').attr('font-weight','800').attr('fill','white')
.text(ev.title.length>16? ev.title.slice(0,16)+'…': ev.title);
// interactivity
fg.on('mouseenter', (e)=>{
const rect = wrap.getBoundingClientRect();
const pt = pos.get(d.id);
// convert svg coords to screen via transform
const transformed = {x: pt.x*zoomTransform.k + zoomTransform.x, y: pt.y*zoomTransform.k + zoomTransform.y};
// approximate screen pos: svg offset + transformed
const svgRect = svg.node().getBoundingClientRect();
const sx = svgRect.left + transformed.x + (18*zoomTransform.k) + 40;
const sy = svgRect.top + transformed.y - (26*zoomTransform.k);
showTooltip(sx, sy, ev, d);
// highlight
highlightEvent(ev.id, true);
}).on('mouseleave', ()=>{
hideTooltip(); highlightEvent(ev.id, false);
}).on('click', (e)=>{
e.stopPropagation();
selectedCommitId = d.id; renderAll();
document.querySelector('.right').scrollTo({top:0, behavior:'smooth'});
});
});
// count badge if >1
if(evts.length>1){
const badge = gFlag.append('g').attr('transform','translate(98, -6)');
badge.append('circle').attr('r',9).attr('fill','#0d1117').attr('stroke', '#8b9bb4');
badge.append('text').attr('text-anchor','middle').attr('dy','0.36em').attr('font-size','9px').attr('font-weight','800').attr('fill','white').text(evts.length);
}
});
// commit id labels below node
allNodes.selectAll('text.clabel').data(d=>[d]).join('text').attr('class','clabel')
.attr('y', 30).attr('text-anchor','middle')
.attr('font-family','JetBrains Mono, monospace').attr('font-size','9px').attr('fill','#8b9bb4')
.text(d=> d.id.slice(0,5));
allNodes.selectAll('text.mlabel').data(d=>[d]).join('text').attr('class','mlabel')
.attr('y', 42).attr('text-anchor','middle')
.attr('font-size','9px').attr('font-weight','600').attr('fill','#e6edf3')
.text(d=> d.message.length>18? d.message.slice(0,18)+'…' : d.message);
nodeSel.exit().remove();
// branch labels at lanes start
const branchLabels = g.selectAll('text.branchLabel').data([...new Set(ordered.map(c=>c.branch))]);
branchLabels.enter().append('text').attr('class','branchLabel')
.merge(branchLabels)
.attr('x',12).attr('y', d=>{
const l = computeLayout().lane.get(d);
return 46 + l*72;
})
.attr('font-size','9px').attr('font-weight','800').attr('letter-spacing','0.08em')
.attr('fill', d=> colorForBranch(d)).text(d=> d.toUpperCase())
.attr('opacity',0.9);
branchLabels.exit().remove();
fitIfNeeded();
}
let fitted=false;
function fitIfNeeded(){
if(fitted) return;
fitted=true;
setTimeout(fitView, 80);
}
function fitView(){
const wrap = document.getElementById('graphWrap');
const w = wrap.clientWidth, h = wrap.clientHeight;
const {ordered, pos} = computeLayout();
if(ordered.length===0) return;
const xs = [...pos.values()].map(p=>p.x), ys=[...pos.values()].map(p=>p.y);
const minX=Math.min(...xs), maxX=Math.max(...xs), minY=Math.min(...ys), maxY=Math.max(...ys);
const pad=60;
const contentW = (maxX-minX)+pad*2, contentH=(maxY-minY)+pad*2;
const scale = Math.min(w/contentW, h/contentH, 1.1);
const tx = (w - contentW*scale)/2 - (minX - pad)*scale;
const ty = (h - contentH*scale)/2 - (minY - pad)*scale;
zoomTransform = d3.zoomIdentity.translate(tx,ty).scale(scale);
svg.transition().duration(500).call(zoom.transform, zoomTransform);
}
function isSelected(id){ return selectedCommitId===id; }
function highlightEvent(evId, on){
document.querySelectorAll('.evt').forEach(el=>{
if(el.dataset.eid===evId) el.style.borderColor = on? 'var(--accent)' : '';
});
}
function showTooltip(x,y, ev, commit){
const t = document.getElementById('tooltip');
t.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<span style="width:8px;height:8px;border-radius:50%;background:${TYPE_COLOR[ev.type]||'#2f81f7'}"></span>
<span style="font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)">${ev.type}</span>
<span class="tag mono">${ev.id}</span>
<span style="margin-left:auto" class="kbd">anchored</span>
</div>
<h4>${ev.title}</h4>
<p>${ev.desc || 'No description.'}</p>
<div class="divider"></div>
<div style="display:flex;gap:8px;align-items:center">
<span class="mono" style="font-size:11px;color:var(--accent);font-weight:800">${commit.id}</span>
<span style="font-size:11px;color:var(--muted)">→ ${commit.message}</span>
</div>
<div style="font-size:10px;color:var(--dim);margin-top:6px">Hover confirms association. Click flag to select commit.</div>
`;
t.style.left = x+'px';
t.style.top = y+'px';
t.classList.add('show');
}
function hideTooltip(){ document.getElementById('tooltip').classList.remove('show'); }
// RENDER LISTS
function renderLists(){
// counts
document.getElementById('branchCount').textContent = state.branches.length;
document.getElementById('commitCount').textContent = state.commits.length;
document.getElementById('eventCount').textContent = state.events.length;
document.getElementById('flowCount').textContent = state.events.length + ' events';
document.getElementById('stats').innerHTML = `
<div class="stat"><span class="dot" style="background:var(--green)"></span><b>${state.commits.length}</b> <span>commits</span></div>
<div class="stat"><span class="dot" style="background:var(--violet)"></span><b>${state.branches.length}</b> <span>branches</span></div>
<div class="stat"><span class="dot" style="background:var(--orange)"></span><b>${state.events.length}</b> <span>anchors</span></div>
`;
// branch list
const bl = document.getElementById('branchList');
bl.innerHTML = state.branches.map(b=>`
<div class="branch-row" style="justify-content:space-between">
<div style="display:flex;align-items:center;gap:8px">
<span class="dot" style="background:${b.color}"></span>
<span class="branch-name">${b.name}</span>
${b.name==='main'?'<span class="pill pill-main">HEAD</span>':''}
</div>
<div style="display:flex;align-items:center;gap:6px">
<span class="branch-meta">${b.target.slice(0,6)}</span>
<button class="btn btn-small" onclick="checkoutBranch('${b.name}')">Checkout</button>
<button class="btn btn-small" onclick="deleteBranch('${b.name}')" style="padding:4px 6px">✕</button>
</div>
</div>
`).join('');
// selects
const commitOptions = state.commits.map(c=> `<option value="${c.id}">${c.id} — ${c.message.slice(0,28)}</option>`).join('');
const branchOptions = state.branches.map(b=> `<option value="${b.name}">${b.name}</option>`).join('');
document.getElementById('newBranchCommit').innerHTML = commitOptions;
document.getElementById('tagCommit').innerHTML = commitOptions;
document.getElementById('cBranch').innerHTML = branchOptions;
document.getElementById('eCommit').innerHTML = `<option value="">— select commit —</option>`+commitOptions;
document.getElementById('cParents').innerHTML = commitOptions;
// commit list
const cl = document.getElementById('commitList');
const order = [...state.commits].reverse();
cl.innerHTML = order.map(c=>{
const isSel = c.id===selectedCommitId;
const col=colorForBranch(c.branch);
return `<div class="item ${isSel?'active':''}" onclick="selectCommit('${c.id}')">
<div class="avatar" style="background:${col}">${c.author.slice(0,2).toUpperCase()}</div>
<div style="flex:1;min-width:0">
<div class="item-title">${c.message}</div>
<div class="item-sub mono">${c.id} • ${c.branch} • ${new Date(c.date).toLocaleDateString()}</div>
</div>
<div style="display:flex;flex-direction:column;gap:4px">
<button class="btn btn-small" onclick="event.stopPropagation(); startEditCommit('${c.id}')">Edit</button>
<button class="btn btn-small" onclick="event.stopPropagation(); deleteCommit('${c.id}')">Del</button>
</div>
</div>`;
}).join('');
// tag list
const tl = document.getElementById('tagList');
if(state.tags.length===0) tl.innerHTML = `<div class="empty">No tags yet</div>`;
else tl.innerHTML = state.tags.map(t=>`
<div class="item"><span class="mono" style="font-size:11px;font-weight:800;color:var(--yellow)">${t.name}</span><span class="mono" style="margin-left:auto;font-size:11px;color:var(--muted)">${t.commitId.slice(0,6)}</span><button class="btn btn-small" onclick="deleteTag('${t.name}')">✕</button></div>
`).join('');
// flow timeline
const flow = document.getElementById('flowList');
const ordered = topologicalOrder(state.commits);
const idxMap = new Map(ordered.map((c,i)=>[c.id,i]));
const sortedEvents = [...state.events].sort((a,b)=> (idxMap.get(a.commitId)??0)-(idxMap.get(b.commitId)??0));
if(sortedEvents.length===0) flow.innerHTML = `<div class="empty">No anchored events</div>`;
else flow.innerHTML = sortedEvents.map(ev=>{
const commit = state.commits.find(c=>c.id===ev.commitId);
const active = commit && commit.id===selectedCommitId;
return `<div class="evt ${active?'active':''}" data-eid="${ev.id}" onclick="selectCommit('${ev.commitId}')" onmouseenter="highlightNode('${ev.commitId}',true)" onmouseleave="highlightNode('${ev.commitId}',false)">
<div class="evt-head">
<span class="evt-title">${ev.title}</span>
<span class="tag" style="border-color:${TYPE_COLOR[ev.type]};color:${TYPE_COLOR[ev.type]}">${ev.type}</span>
</div>
<div class="evt-desc">${ev.desc||''}</div>
<div style="display:flex;gap:6px;align-items:center;margin-top:8px">
<span class="kbd">${ev.commitId.slice(0,6)}</span>
<span class="mini mono" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1">${commit?commit.message:'(missing commit)'}</span>
<button class="btn btn-small" onclick="event.stopPropagation(); startEditEvent('${ev.id}')">Edit</button>
<button class="btn btn-small" onclick="event.stopPropagation(); deleteEvent('${ev.id}')">✕</button>
</div>
</div>`;
}).join('');
// context grid view
const cg = document.getElementById('contextGrid');
const filter = document.getElementById('filterType').value;
let filtered = sortedEvents;
if(filter!=='all') filtered = filtered.filter(e=>e.type===filter);
cg.innerHTML = filtered.map(ev=>{
const commit = state.commits.find(c=>c.id===ev.commitId);
return `<div class="evt active" style="margin:0">
<div class="evt-head"><span class="evt-title">${ev.title}</span><span class="tag" style="background:${TYPE_COLOR[ev.type]};color:white;border-color:transparent">${ev.type}</span></div>
<div class="evt-desc">${ev.desc||''}</div>
<div class="divider"></div>
<div style="display:flex;gap:8px;align-items:center"><span class="mono" style="font-size:11px;font-weight:800;color:var(--accent)">${ev.commitId}</span><span class="mini">${commit?commit.message:''}</span></div>
<div style="margin-top:8px;display:flex;gap:6px"><button class="btn btn-small" onclick="selectCommit('${ev.commitId}'); document.querySelector('[data-tab=graph]').click()">Show in Graph →</button><button class="btn btn-small" onclick="startEditEvent('${ev.id}')">Edit</button></div>
</div>`;
}).join('') || `<div class="empty">No events for filter</div>`;
// selected panel
const sp = document.getElementById('selectedPanel');
const sel = state.commits.find(c=>c.id===selectedCommitId);
document.getElementById('selectedId').textContent = selectedCommitId? selectedCommitId.slice(0,7): 'none';
if(!sel){
sp.innerHTML = `<div class="empty">Select a commit on the graph to see details and its anchored markers. Hover flags for confirmation.</div>
<div class="detail-card" style="margin-top:10px"><div style="font-size:11px;font-weight:800;color:var(--muted)">INTEGRATED ANCHORING</div><div style="font-size:12px;font-weight:700;margin-top:6px">Events are spatially anchored to nodes</div><div class="mini" style="margin-top:4px">Each external contextual event stores an Association ID linking it to a Git commit. On the graph, flags sit directly on their commit nodes.</div></div>`;
} else {
const evts = state.events.filter(e=>e.commitId===sel.id);
const parents = sel.parents.join(', ') || '— root —';
sp.innerHTML = `
<div class="detail-card">
<div class="commit-big">${sel.id}</div>
<div style="font-size:14px;font-weight:800;margin:6px 0;line-height:1.25">${sel.message}</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin:8px 0">
<span class="pill" style="background:${colorForBranch(sel.branch)};color:white;border-color:transparent">${sel.branch}</span>
<span class="tag mono">${sel.author}</span>
<span class="tag mono">${new Date(sel.date).toLocaleString()}</span>
</div>
<div class="mini"><b>Parents:</b> <span class="mono">${parents}</span></div>
<div class="mini"><b>Children:</b> <span class="mono">${state.commits.filter(c=>c.parents.includes(sel.id)).map(c=>c.id.slice(0,5)).join(', ')||'— tip —'}</span></div>
<div style="margin-top:10px;display:flex;gap:6px">
<button class="btn btn-small" onclick="startEditCommit('${sel.id}')">Edit Commit</button>
<button class="btn btn-small" onclick="deleteCommit('${sel.id}')">Delete</button>
<button class="btn btn-small" onclick="duplicateCommit('${sel.id}')">Duplicate</button>
</div>
</div>
<div style="margin-top:12px">
<div style="font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:6px">Anchored Context <span class="badge">${evts.length}</span></div>
${evts.length? evts.map(ev=>`
<div class="evt active" style="margin-top:8px" data-eid="${ev.id}">
<div class="evt-head"><span class="evt-title">${ev.title}</span><span class="tag" style="color:${TYPE_COLOR[ev.type]};border-color:${TYPE_COLOR[ev.type]}">${ev.type}</span></div>
<div class="evt-desc">${ev.desc}</div>
<div style="margin-top:8px;display:flex;gap:6px"><button class="btn btn-small" onclick="startEditEvent('${ev.id}')">Edit</button><button class="btn btn-small" onclick="deleteEvent('${ev.id}')">Unanchor</button></div>
</div>
`).join('') : `<div class="empty" style="margin-top:8px">No events anchored here. Add one from the left panel.</div>`}
</div>
`;
}
}
function renderAll(){
save();
renderGraph();
renderLists();
}
function selectCommit(id){ selectedCommitId=id; renderAll(); }
function highlightNode(id,on){
g.selectAll('g.node').filter(d=>d.id===id).select('circle.core').attr('stroke-width', on?4: (isSelected(id)?3:2));
}
// CRUD
function createCommit(){
const message=document.getElementById('cMessage').value.trim();
const author=document.getElementById('cAuthor').value.trim()||'you';
const branch=document.getElementById('cBranch').value;
const sel = [...document.getElementById('cParents').selectedOptions].map(o=>o.value);
if(!message) return alert('Message required');
const id = uid('c_');
const parents = sel.length? sel : (()=>{
const b = state.branches.find(x=>x.name===branch);
return b? [b.target] : [];
})();
const commit = {id, message, author, date:new Date().toISOString(), parents, branch};
state.commits.push(commit);
// move branch pointer
const br = state.branches.find(b=>b.name===branch);
if(br) br.target=id;
document.getElementById('cMessage').value='';
selectedCommitId=id;
renderAll();
}
function startEditCommit(id){
const c=state.commits.find(x=>x.id===id); if(!c) return;
editingCommitId=id;
document.getElementById('cMessage').value=c.message;
document.getElementById('cAuthor').value=c.author;
document.getElementById('cBranch').value=c.branch;
// set parents
const selEl=document.getElementById('cParents');
[...selEl.options].forEach(o=> o.selected = c.parents.includes(o.value));
document.getElementById('createCommitBtn').style.display='none';
document.getElementById('editCommitBtn').style.display='inline-flex';
document.getElementById('cancelEditBtn').style.display='inline-flex';
}
function saveEditCommit(){
if(!editingCommitId) return;
const c=state.commits.find(x=>x.id===editingCommitId);
c.message=document.getElementById('cMessage').value.trim()||c.message;
c.author=document.getElementById('cAuthor').value.trim()||c.author;
c.branch=document.getElementById('cBranch').value;
c.parents=[...document.getElementById('cParents').selectedOptions].map(o=>o.value);
// if parents empty keep at least none
editingCommitId=null;
document.getElementById('createCommitBtn').style.display='inline-flex';
document.getElementById('editCommitBtn').style.display='none';
document.getElementById('cancelEditBtn').style.display='none';
document.getElementById('cMessage').value='';
renderAll();
}
function cancelEditCommit(){
editingCommitId=null;
document.getElementById('createCommitBtn').style.display='inline-flex';
document.getElementById('editCommitBtn').style.display='none';
document.getElementById('cancelEditBtn').style.display='none';
document.getElementById('cMessage').value='';
document.getElementById('cAuthor').value='';
}
function deleteCommit(id){
const hasChildren = state.commits.some(c=>c.parents.includes(id));
if(hasChildren) return alert('Cannot delete: commit has children. Delete or rebase children first.');
if(!confirm('Delete commit '+id+' ? This will also remove its anchored events.')) return;
state.commits = state.commits.filter(c=>c.id!==id);
state.events = state.events.filter(e=>e.commitId!==id);
// fix branches pointing to deleted
state.branches.forEach(b=>{ if(b.target===id) b.target = state.commits[state.commits.length-1]?.id || id; });
if(selectedCommitId===id) selectedCommitId=null;
renderAll();
}
function duplicateCommit(id){
const c=state.commits.find(x=>x.id===id); const nid=uid('c_');
state.commits.push({...c, id:nid, message:c.message+' (copy)', date:new Date().toISOString()});
selectedCommitId=nid; renderAll();
}
function createBranch(){
const name=document.getElementById('newBranchName').value.trim();
const target=document.getElementById('newBranchCommit').value;
if(!name) return alert('Branch name required');
if(state.branches.some(b=>b.name===name)) return alert('Branch exists');
state.branches.push({name, target, color: colorForBranch(name) });
document.getElementById('newBranchName').value='';
renderAll();
}
function deleteBranch(name){
if(name==='main') return alert('Cannot delete main');
if(!confirm('Delete branch '+name+' ?')) return;
state.branches=state.branches.filter(b=>b.name!==name);
renderAll();
}
function checkoutBranch(name){
const b=state.branches.find(x=>x.name===name);
if(b) { selectedCommitId=b.target; document.getElementById('cBranch').value=name; renderAll(); }
}
function createEvent(){
const title=document.getElementById('eTitle').value.trim();
const type=document.getElementById('eType').value;
const commitId=document.getElementById('eCommit').value;
const desc=document.getElementById('eDesc').value.trim();
if(!title) return alert('Title required');
if(!commitId) return alert('Association ID (commit) is mandatory');
const ev={id:uid('e'), title, type, commitId, desc};
state.events.push(ev);
document.getElementById('eTitle').value=''; document.getElementById('eDesc').value='';
renderAll();
}
function startEditEvent(id){
const ev=state.events.find(e=>e.id===id); if(!ev) return;
editingEventId=id;
document.getElementById('eTitle').value=ev.title;
document.getElementById('eType').value=ev.type;
document.getElementById('eCommit').value=ev.commitId;
document.getElementById('eDesc').value=ev.desc;
document.getElementById('createEventBtn').style.display='none';
document.getElementById('saveEventEditBtn').style.display='inline-flex';
document.getElementById('cancelEventEditBtn').style.display='inline-flex';
}
function saveEventEdit(){
if(!editingEventId) return;
const ev=state.events.find(e=>e.id===editingEventId);
ev.title=document.getElementById('eTitle').value.trim()||ev.title;
ev.type=document.getElementById('eType').value;
ev.commitId=document.getElementById('eCommit').value||ev.commitId;
ev.desc=document.getElementById('eDesc').value.trim();
editingEventId=null;
document.getElementById('createEventBtn').style.display='inline-flex';
document.getElementById('saveEventEditBtn').style.display='none';
document.getElementById('cancelEventEditBtn').style.display='none';
document.getElementById('eTitle').value=''; document.getElementById('eDesc').value='';
renderAll();
}
function cancelEventEdit(){
editingEventId=null;
document.getElementById('createEventBtn').style.display='inline-flex';
document.getElementById('saveEventEditBtn').style.display='none';
document.getElementById('cancelEventEditBtn').style.display='none';
document.getElementById('eTitle').value=''; document.getElementById('eDesc').value='';
}
function deleteEvent(id){
state.events=state.events.filter(e=>e.id!==id);
renderAll();
}
function createTag(){
const name=document.getElementById('tagName').value.trim();
const commitId=document.getElementById('tagCommit').value;
if(!name) return alert('Tag name required');
state.tags.push({name, commitId, color:'#d29922'});
document.getElementById('tagName').value='';
renderAll();
}
function deleteTag(name){ state.tags=state.tags.filter(t=>t.name!==name); renderAll(); }
// import/export
function exportState(){
const blob = new Blob([JSON.stringify(state,null,2)],{type:'application/json'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a'); a.href=url; a.download='git-topology.json'; a.click(); URL.revokeObjectURL(url);
}
function importState(e){
const file=e.target.files[0]; if(!file) return;
const r=new FileReader();
r.onload=()=>{
try{
const data=JSON.parse(r.result);
if(!data.commits || !data.events) throw new Error('Invalid file');
state=data; if(!state.tags) state.tags=[];
selectedCommitId = state.commits[0]?.id || null;
save(); renderAll();
}catch(err){ alert('Import failed: '+err.message); }
};
r.readAsText(file);
e.target.value='';
}
// init
state = load();
selectedCommitId = state.commits[state.commits.length-1]?.id || state.commits[0]?.id || null;
initGraph();
renderAll();
// events
document.getElementById('createCommitBtn').addEventListener('click', createCommit);
document.getElementById('editCommitBtn').addEventListener('click', saveEditCommit);
document.getElementById('cancelEditBtn').addEventListener('click', cancelEditCommit);
document.getElementById('clearCommitBtn').addEventListener('click', cancelEditCommit);
document.getElementById('createBranchBtn').addEventListener('click', createBranch);
document.getElementById('clearBranchBtn').addEventListener('click', ()=> document.getElementById('newBranchName').value='');
document.getElementById('addBranchBtn').addEventListener('click', ()=> document.getElementById('newBranchName').focus());
document.getElementById('createEventBtn').addEventListener('click', createEvent);
document.getElementById('saveEventEditBtn').addEventListener('click', saveEventEdit);
document.getElementById('cancelEventEditBtn').addEventListener('click', cancelEventEdit);
document.getElementById('clearEventBtn').addEventListener('click', cancelEventEdit);
document.getElementById('createTagBtn').addEventListener('click', createTag);
document.getElementById('exportBtn').addEventListener('click', exportState);
document.getElementById('importFile').addEventListener('change', importState);
document.getElementById('resetBtn').addEventListener('click', ()=>{ if(confirm('Reset to demo data?')){ localStorage.removeItem(STORAGE_KEY); state=seed(); selectedCommitId=state.commits[0].id; save(); fitted=false; renderAll(); }});
document.getElementById('newCommitBtn').addEventListener('click', ()=> document.getElementById('cMessage').focus());
document.getElementById('fitBtn').addEventListener('click', fitView);
document.getElementById('cFit').addEventListener('click', fitView);
document.getElementById('cZoomIn').addEventListener('click', ()=> svg.transition().call(zoom.scaleBy, 1.2));
document.getElementById('cZoomOut').addEventListener('click', ()=> svg.transition().call(zoom.scaleBy, 0.8));
document.getElementById('zoomIn').addEventListener('click', ()=> svg.transition().call(zoom.scaleBy, 1.2));
document.getElementById('zoomOut').addEventListener('click', ()=> svg.transition().call(zoom.scaleBy, 0.8));
document.getElementById('focusGraphBtn').addEventListener('click', fitView);
document.getElementById('filterType').addEventListener('change', renderLists);
document.querySelectorAll('.tab').forEach(t=> t.addEventListener('click', ()=>{
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
t.classList.add('active');
const isContext = t.dataset.tab==='context';
document.getElementById('graphWrap').style.display = isContext? 'none':'block';
document.getElementById('contextView').style.display = isContext? 'block':'none';
}));
window.selectCommit=selectCommit;
window.startEditCommit=startEditCommit;
window.deleteCommit=deleteCommit;
window.deleteBranch=deleteBranch;
window.checkoutBranch=checkoutBranch;
window.startEditEvent=startEditEvent;
window.deleteEvent=deleteEvent;
window.deleteTag=deleteTag;
window.duplicateCommit=duplicateCommit;
window.highlightNode=highlightNode;
window.addEventListener('resize', ()=>{ fitted=false; renderGraph(); });
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"6236ceb5-2385-46d8-988d-37b55daa3c5d","tokensIn":799,"tokensOut":17304,"tokensTotal":18103,"cost":0.07441645,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:27:20.596Z"} -->