Git Explorer
Qwen 3.6 27b MTP · typescript

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 54,089 bytes · SHA-256 4671d3edef52
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Git & Structure Visualizer</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; }
#app { display: flex; flex-direction: column; width: 100%; height: 100%; }
#toolbar {
display: flex; align-items: center; gap: 8px; padding: 8px 16px;
background: #161b22; border-bottom: 1px solid #30363d; z-index: 10; flex-shrink: 0;
}
.tool-btn {
padding: 6px 14px; border: 1px solid #30363d; border-radius: 6px; background: #21262d;
color: #c9d1d9; cursor: pointer; font-size: 13px; transition: all .15s; white-space: nowrap;
}
.tool-btn:hover { background: #30363d; border-color: #8b949e; }
.tool-btn.danger { color: #f85149; }
.tool-btn.danger:hover { background: #2a1215; border-color: #f85149; }
.toggle-group { margin-left: auto; display: flex; border-radius: 6px; overflow: hidden; border: 1px solid #30363d; }
.toggle-group button {
padding: 6px 16px; background: #21262d; color: #8b949e; border: none; cursor: pointer; font-size: 13px; transition: all .15s;
}
.toggle-group button.active { background: #1f6feb; color: #fff; }
.toggle-group button:hover:not(.active) { background: #30363d; color: #c9d1d9; }
#main-area { flex: 1; position: relative; overflow: hidden; display: flex; }
#graph-container, #timeline-container { width: 100%; height: 100%; }
#sidebar {
width: 260px; background: #161b22; border-left: 1px solid #30363d; overflow-y: auto;
flex-shrink: 0; padding: 12px; display: flex; flex-direction: column; gap: 16px;
}
#sidebar h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 8px; }
#branches-ul, #events-ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.branch-item, .event-item {
padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: background .15s;
display: flex; align-items: center; gap: 8px;
}
.branch-item:hover, .event-item:hover { background: #21262d; }
.branch-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
#detail-panel { padding-bottom: 12px; border-bottom: 1px solid #30363d; }
#detail-content .hint { color: #484f58; font-style: italic; font-size: 13px; }
.detail-row { margin-bottom: 8px; font-size: 13px; }
.detail-label { color: #8b949e; font-weight: 600; display: block; margin-bottom: 2px; }
.detail-value { word-break: break-all; }
/* Modal */
#modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 100;
display: flex; align-items: center; justify-content: center;
}
#modal-box {
background: #161b22; border: 1px solid #30363d; border-radius: 12px; width: 480px; max-width: 90vw;
max-height: 80vh; overflow-y: auto; box-shadow: 0 16px 48px rgba(0,0,0,.4);
}
#modal-header {
display: flex; align-items: center; justify-content: space-between; padding: 16px 20px;
border-bottom: 1px solid #30363d; font-weight: 600; font-size: 15px;
}
.close-btn { background: none; border: none; color: #8b949e; font-size: 20px; cursor: pointer; padding: 0 4px; }
.close-btn:hover { color: #f85149; }
#modal-body { padding: 20px; }
#modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid #30363d; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: .3px; }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
color: #c9d1d9; font-size: 14px; outline: none; transition: border-color .15s;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: #1f6feb; }
.form-group textarea { resize: vertical; min-height: 60px; font-family: inherit; }
.primary { padding: 8px 20px; background: #1f6feb; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; }
.primary:hover { background: #388bfd; }
.secondary { padding: 8px 20px; background: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; cursor: pointer; font-size: 13px; }
.secondary:hover { background: #30363d; }
/* Timeline */
.timeline-track { padding: 20px; overflow-y: auto; height: 100%; }
.timeline-item {
display: flex; gap: 16px; margin-bottom: 16px; position: relative; padding-left: 32px;
}
.timeline-item::before {
content: ''; position: absolute; left: 15px; top: 0; bottom: -16px; width: 2px; background: #30363d;
}
.timeline-marker {
position: absolute; left: 8px; top: 4px; width: 16px; height: 16px; border-radius: 50%;
border: 2px solid #1f6feb; background: #0d1117; z-index: 1; cursor: pointer; transition: all .15s;
}
.timeline-marker:hover { transform: scale(1.3); }
.timeline-marker.event-marker { border-color: #a371f7; background: rgba(163,113,247,.13); }
.timeline-card {
flex: 1; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; cursor: pointer; transition: all .15s;
}
.timeline-card:hover { border-color: #8b949e; background: #1c2129; }
.timeline-card h4 { font-size: 14px; margin-bottom: 4px; }
.timeline-card p { font-size: 12px; color: #8b949e; }
/* SVG styles */
.link-path { fill: none; stroke-width: 2px; opacity: .6; transition: all .2s; }
.commit-node { cursor: pointer; transition: filter .15s; }
.commit-node:hover { filter: brightness(1.3); }
.commit-ring { fill: #0d1117; stroke-width: 2px; }
.commit-inner { stroke: none; }
.commit-label { font-size: 10px; fill: #8b949e; pointer-events: none; text-anchor: middle; }
.branch-label { font-size: 11px; font-weight: 600; pointer-events: none; }
.event-badge { cursor: pointer; transition: filter .15s; }
.event-line { stroke-dasharray: 4,3; opacity: .5; }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #0d1117; }
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
@media (max-width: 900px) {
#sidebar { width: 200px; }
}
</style>
</head>
<body>
<div id="toolbar">
<button id="btn-new-commit" class="tool-btn" title="New Commit">➕ Commit</button>
<button id="btn-new-branch" class="tool-btn" title="New Branch">🌿 Branch</button>
<button id="btn-new-event" class="tool-btn" title="New Event">📌 Event</button>
<button id="btn-import" class="tool-btn" title="Import Data">📥 Import</button>
<button id="btn-export" class="tool-btn" title="Export Data">📤 Export</button>
<button id="btn-reset" class="tool-btn danger" title="Reset All">🗑 Reset</button>
<span id="view-toggle" class="toggle-group">
<button id="view-graph" class="active">Graph</button>
<button id="view-timeline">Timeline</button>
</span>
</div>
<div id="main-area">
<div id="graph-container"></div>
<div id="timeline-container" style="display:none;"></div>
</div>
<div id="sidebar">
<div id="detail-panel">
<h3>Details</h3>
<div id="detail-content"><p class="hint">Select a commit or event to view details.</p></div>
</div>
<div id="branch-list">
<h3>Branches</h3>
<ul id="branches-ul"></ul>
</div>
<div id="event-list">
<h3>Events</h3>
<ul id="events-ul"></ul>
</div>
</div>
<div id="modal-overlay" style="display:none;">
<div id="modal-box">
<div id="modal-header"><span id="modal-title"></span><button id="modal-close" class="close-btn">×</button></div>
<div id="modal-body"></div>
<div id="modal-footer"><button id="modal-cancel" class="secondary">Cancel</button><button id="modal-save" class="primary">Save</button></div>
</div>
</div>
<input type="file" id="file-input" accept=".json" style="display:none;">
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"../state":"state.ts","../graph-renderer":"graph-renderer.ts","../timeline-view":"timeline-view.ts","../ui-panels":"ui-panels.ts"},"state.ts":{"./types":"types.ts"},"graph-renderer.ts":{"./state":"state.ts","./types":"types.ts"},"timeline-view.ts":{"./state":"state.ts","./types":"types.ts"},"ui-panels.ts":{"./state":"state.ts","./types":"types.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("../state");
const graph_renderer_1 = require("../graph-renderer");
const timeline_view_1 = require("../timeline-view");
const ui_panels_1 = require("../ui-panels");
// --- Initialize ---
function init() {
// Init graph visualization
(0, graph_renderer_1.initGraph)('graph-container');
// Subscribe to state changes
(0, state_1.subscribe)(onStateChange);
// Initial render
onStateChange();
// Wire up toolbar buttons
document.getElementById('btn-new-commit').addEventListener('click', ui_panels_1.showNewCommitModal);
document.getElementById('btn-new-branch').addEventListener('click', ui_panels_1.showNewBranchModal);
document.getElementById('btn-new-event').addEventListener('click', ui_panels_1.showNewEventModal);
// View toggle
document.getElementById('view-graph').addEventListener('click', () => switchView('graph'));
document.getElementById('view-timeline').addEventListener('click', () => switchView('timeline'));
// Import/Export
document.getElementById('btn-import').addEventListener('click', () => {
const fileInput = document.getElementById('file-input');
fileInput.click();
});
document.getElementById('file-input').addEventListener('change', (e) => {
const files = e.target.files;
if (files && files.length > 0)
(0, ui_panels_1.handleImport)(files[0]);
});
document.getElementById('btn-export').addEventListener('click', ui_panels_1.handleExport);
document.getElementById('btn-reset').addEventListener('click', () => {
if (confirm('Reset all data to defaults? This cannot be undone.'))
(0, state_1.resetState)();
});
// Resize handler
window.addEventListener('resize', () => {
(0, graph_renderer_1.resizeGraph)();
onStateChange();
});
}
function switchView(view) {
(0, ui_panels_1.setView)(view);
document.getElementById('view-graph').classList.toggle('active', view === 'graph');
document.getElementById('view-timeline').classList.toggle('active', view === 'timeline');
document.getElementById('graph-container').style.display = view === 'graph' ? '' : 'none';
document.getElementById('timeline-container').style.display = view === 'timeline' ? '' : 'none';
onStateChange();
}
function onStateChange() {
(0, ui_panels_1.renderSidebar)();
if ((0, ui_panels_1.getCurrentView)() === 'graph') {
(0, graph_renderer_1.renderGraph)();
}
else {
(0, timeline_view_1.renderTimeline)();
}
}
// Start the app
init();
};
// ── module: types.ts ──
__mods["types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVENT_ICONS = exports.EVENT_COLORS = exports.BRANCH_COLORS = void 0;
exports.generateId = generateId;
exports.shortHash = shortHash;
exports.BRANCH_COLORS = [
'#1f6feb', '#a371f7', '#238636', '#db6d28', '#f85149',
'#58a6ff', '#bc8cff', '#3fb950', '#f0883e', '#ff7b72',
];
exports.EVENT_COLORS = {
milestone: '#a371f7',
freeze: '#db6d28',
release: '#238636',
review: '#58a6ff',
incident: '#f85149',
other: '#8b949e',
};
exports.EVENT_ICONS = {
milestone: '🏁',
freeze: '❄️',
release: '🚀',
review: '🔍',
incident: '⚠️',
other: '📌',
};
function generateId() {
return Math.random().toString(16).slice(2, 10);
}
function shortHash(id) {
return id.slice(0, 7);
}
};
// ── types: d3-declarations.d.ts ──
__mods["d3-declarations.d.ts"] = function (exports) {};
// ── module: state.ts ──
__mods["state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createDefaultState = createDefaultState;
exports.getState = getState;
exports.subscribe = subscribe;
exports.addCommit = addCommit;
exports.updateCommit = updateCommit;
exports.deleteCommit = deleteCommit;
exports.addBranch = addBranch;
exports.updateBranch = updateBranch;
exports.deleteBranch = deleteBranch;
exports.addEvent = addEvent;
exports.updateEvent = updateEvent;
exports.deleteEvent = deleteEvent;
exports.selectCommit = selectCommit;
exports.selectEvent = selectEvent;
exports.importState = importState;
exports.exportState = exportState;
exports.resetState = resetState;
const types_1 = require("./types");
const STORAGE_KEY = 'git-visualizer-state';
let state;
let listeners = new Set();
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw)
return JSON.parse(raw);
}
catch { }
return createDefaultState();
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
catch { }
}
function createDefaultState() {
const c1 = (0, types_1.generateId)(), c2 = (0, types_1.generateId)(), c3 = (0, types_1.generateId)(), c4 = (0, types_1.generateId)();
const c5 = (0, types_1.generateId)(), c6 = (0, types_1.generateId)(), c7 = (0, types_1.generateId)();
return {
commits: [
{ id: c1, message: 'Initial commit', parents: [], author: 'Alice', date: new Date(Date.now() - 7 * 86400000).toISOString() },
{ id: c2, message: 'Add authentication module', parents: [c1], author: 'Bob', date: new Date(Date.now() - 5 * 86400000).toISOString() },
{ id: c3, message: 'Implement user profiles', parents: [c2], author: 'Alice', date: new Date(Date.now() - 4 * 86400000).toISOString() },
{ id: c4, message: 'Add API endpoints', parents: [c2], author: 'Charlie', date: new Date(Date.now() - 3 * 86400000).toISOString() },
{ id: c5, message: 'Merge feature branch', parents: [c3, c4], author: 'Alice', date: new Date(Date.now() - 2 * 86400000).toISOString(), tag: 'v1.0' },
{ id: c6, message: 'Fix login bug', parents: [c5], author: 'Bob', date: new Date(Date.now() - 1 * 86400000).toISOString() },
{ id: c7, message: 'Add dashboard analytics', parents: [c6], author: 'Charlie', date: new Date().toISOString(), tag: 'v1.1' },
],
branches: [
{ name: 'main', commitId: c7, color: types_1.BRANCH_COLORS[0] },
{ name: 'feature/auth', commitId: c3, color: types_1.BRANCH_COLORS[1] },
{ name: 'feature/api', commitId: c4, color: types_1.BRANCH_COLORS[2] },
],
events: [
{ id: (0, types_1.generateId)(), title: 'Project Kickoff', description: 'Initial project setup and planning', commitId: c1, type: 'milestone' },
{ id: (0, types_1.generateId)(), title: 'Code Freeze v1.0', description: 'No new features before release', commitId: c5, type: 'freeze' },
{ id: (0, types_1.generateId)(), title: 'Release v1.0', description: 'First production release', commitId: c5, type: 'release' },
{ id: (0, types_1.generateId)(), title: 'Security Review', description: 'Auth module security audit passed', commitId: c3, type: 'review' },
],
};
}
function getState() { return state; }
function subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function notify() {
saveState();
for (const fn of listeners)
fn();
}
// --- Commit CRUD ---
function addCommit(commit) {
const c = { ...commit, id: (0, types_1.generateId)(), date: commit.date || new Date().toISOString() };
state.commits.push(c);
notify();
return c;
}
function updateCommit(id, updates) {
const idx = state.commits.findIndex(c => c.id === id);
if (idx >= 0) {
Object.assign(state.commits[idx], updates);
notify();
}
}
function deleteCommit(id) {
state.commits = state.commits.filter(c => c.id !== id);
// Remove events pointing to deleted commit
state.events = state.events.filter(e => e.commitId !== id);
// Update branches pointing to deleted commit
for (const b of state.branches) {
if (b.commitId === id) {
const parents = state.commits.find(c => c.id === id)?.parents || [];
b.commitId = parents[0] || '';
}
}
// Remove references in parent lists
for (const c of state.commits) {
c.parents = c.parents.filter(p => p !== id);
}
if (state.selectedCommitId === id)
state.selectedCommitId = undefined;
notify();
}
// --- Branch CRUD ---
function addBranch(name, commitId) {
const color = types_1.BRANCH_COLORS[state.branches.length % types_1.BRANCH_COLORS.length];
const b = { name, commitId, color };
state.branches.push(b);
notify();
return b;
}
function updateBranch(name, updates) {
const idx = state.branches.findIndex(b => b.name === name);
if (idx >= 0) {
Object.assign(state.branches[idx], updates);
notify();
}
}
function deleteBranch(name) {
state.branches = state.branches.filter(b => b.name !== name);
notify();
}
// --- Event CRUD ---
function addEvent(event) {
const e = { ...event, id: (0, types_1.generateId)() };
state.events.push(e);
notify();
return e;
}
function updateEvent(id, updates) {
const idx = state.events.findIndex(e => e.id === id);
if (idx >= 0) {
Object.assign(state.events[idx], updates);
notify();
}
}
function deleteEvent(id) {
state.events = state.events.filter(e => e.id !== id);
if (state.selectedEventId === id)
state.selectedEventId = undefined;
notify();
}
// --- Selection ---
function selectCommit(id) {
state.selectedCommitId = id;
notify();
}
function selectEvent(id) {
state.selectedEventId = id;
notify();
}
// --- Import/Export ---
function importState(data) {
state = data;
notify();
}
function exportState() {
return JSON.stringify(state, null, 2);
}
function resetState() {
state = createDefaultState();
notify();
}
// Initialize
state = loadState();
};
// ── module: graph-renderer.ts ──
__mods["graph-renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initGraph = initGraph;
exports.renderGraph = renderGraph;
exports.resizeGraph = resizeGraph;
const state_1 = require("./state");
const types_1 = require("./types");
let svg;
let g; // zoomable group
let width = 0;
let height = 0;
function initGraph(containerId) {
const container = document.getElementById(containerId);
width = container.clientWidth;
height = container.clientHeight;
svg = d3.select('#' + containerId)
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.style('background', '#0d1117');
// Defs for arrow markers and glow filter
const defs = svg.append('defs');
// Arrow marker
const marker = defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 28)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto');
marker.append('path')
.attr('d', 'M0,-4L8,0L0,4')
.attr('fill', '#30363d');
// Glow filter
const glow = defs.append('filter').attr('id', 'glow').attr('x', '-50%').attr('y', '-50%').attr('width', '200%').attr('height', '200%');
glow.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'coloredBlur');
const feMerge = glow.append('feMerge');
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
g = svg.append('g');
// Zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.2, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Double-click to reset zoom
svg.on('dblclick.zoom', () => {
svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
});
}
function renderGraph() {
const state = (0, state_1.getState)();
const commits = state.commits;
const branches = state.branches;
const events = state.events;
// Build link data from parent relationships
const links = [];
for (const c of commits) {
for (const p of c.parents) {
if (commits.find(x => x.id === p)) {
links.push({ source: p, target: c.id });
}
}
}
const nodeData = commits.map(c => ({ ...c, radius: 14 }));
const linkData = links;
// Build event lookup per commit
const eventsByCommit = {};
for (const e of events) {
if (!eventsByCommit[e.commitId])
eventsByCommit[e.commitId] = [];
eventsByCommit[e.commitId].push(e);
}
// Build branch lookup per commit
const branchesAtCommit = {};
for (const b of branches) {
if (!branchesAtCommit[b.commitId])
branchesAtCommit[b.commitId] = [];
branchesAtCommit[b.commitId].push(b);
}
// Clear previous
g.selectAll('*').remove();
const linkGroup = g.append('g').attr('class', 'links');
const nodeGroup = g.append('g').attr('class', 'nodes');
const eventGroup = g.append('g').attr('class', 'events');
const labelGroup = g.append('g').attr('class', 'labels');
// --- Links ---
const linkSel = linkGroup.selectAll('.link-path')
.data(linkData)
.enter()
.append('path')
.attr('class', 'link-path')
.attr('stroke', '#30363d')
.attr('marker-end', 'url(#arrowhead)');
// --- Nodes ---
const nodeSel = nodeGroup.selectAll('.commit-node')
.data(nodeData)
.enter()
.append('g')
.attr('class', 'commit-node');
// Outer ring (branch color if branch tip)
nodeSel.append('circle')
.attr('class', 'commit-ring')
.attr('r', 18)
.attr('stroke', (d) => {
const bs = branchesAtCommit[d.id];
return bs ? bs[0].color : '#30363d';
})
.attr('fill', '#0d1117');
// Inner circle
nodeSel.append('circle')
.attr('class', 'commit-inner')
.attr('r', 12)
.attr('fill', (d) => {
const bs = branchesAtCommit[d.id];
return bs ? bs[0].color + '44' : '#30363d';
});
// Hash label inside node
nodeSel.append('text')
.attr('class', 'commit-label')
.attr('dy', '0.35em')
.attr('fill', '#c9d1d9')
.attr('font-size', '8px')
.attr('font-family', 'monospace')
.text((d) => (0, types_1.shortHash)(d.id).slice(0, 4));
// Tag badge if present
nodeSel.filter((d) => !!d.tag)
.append('text')
.attr('class', 'commit-label')
.attr('dy', -26)
.attr('fill', '#f0883e')
.attr('font-size', '10px')
.attr('font-weight', 'bold')
.text((d) => d.tag);
// Click handler
nodeSel.on('click', (_e, d) => {
(0, state_1.selectCommit)(state.selectedCommitId === d.id ? undefined : d.id);
});
const eventNodes = [];
for (const [commitId, evts] of Object.entries(eventsByCommit)) {
evts.forEach((evt, i) => {
eventNodes.push({
id: evt.id,
title: evt.title,
type: evt.type,
commitId,
angle: (i / Math.max(evts.length, 1)) * Math.PI * 2 - Math.PI / 2,
dist: 30 + i * 8,
});
});
}
const eventSel = eventGroup.selectAll('.event-badge')
.data(eventNodes)
.enter()
.append('g')
.attr('class', 'event-badge');
// Dashed line from commit to event
eventSel.append('line')
.attr('class', 'event-line')
.attr('stroke', (d) => types_1.EVENT_COLORS[d.type])
.attr('x1', 0).attr('y1', 0)
.attr('x2', (d) => Math.cos(d.angle) * d.dist)
.attr('y2', (d) => Math.sin(d.angle) * d.dist);
// Event icon circle
eventSel.append('circle')
.attr('r', 10)
.attr('cx', (d) => Math.cos(d.angle) * d.dist)
.attr('cy', (d) => Math.sin(d.angle) * d.dist)
.attr('fill', (d) => types_1.EVENT_COLORS[d.type] + '33')
.attr('stroke', (d) => types_1.EVENT_COLORS[d.type])
.attr('stroke-width', 2);
// Event icon text
eventSel.append('text')
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.attr('font-size', '10px')
.attr('x', (d) => Math.cos(d.angle) * d.dist)
.attr('y', (d) => Math.sin(d.angle) * d.dist + 1)
.text((d) => types_1.EVENT_ICONS[d.type]);
// Event title label
eventSel.append('text')
.attr('class', 'commit-label')
.attr('fill', '#c9d1d9')
.attr('font-size', '9px')
.attr('x', (d) => Math.cos(d.angle) * (d.dist + 14))
.attr('y', (d) => Math.sin(d.angle) * d.dist + 3)
.text((d) => d.title);
eventSel.on('click', (_e, d) => {
(0, state_1.selectEvent)(state.selectedEventId === d.id ? undefined : d.id);
(0, state_1.selectCommit)(d.commitId);
});
// --- Branch labels ---
const branchLabelData = branches.filter(b => commits.find(c => c.id === b.commitId));
const labelSel = labelGroup.selectAll('.branch-label-group')
.data(branchLabelData)
.enter()
.append('g')
.attr('class', 'branch-label-group');
labelSel.append('text')
.attr('class', 'branch-label')
.attr('fill', (d) => d.color)
.attr('dy', -24)
.attr('x', 0)
.text((d) => '\u25cf ' + d.name);
// --- Force simulation ---
const sim = d3.forceSimulation(nodeData)
.force('link', d3.forceLink(linkData).id((d) => d.id).distance(120))
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide(50));
sim.on('tick', () => {
// Update links with curved paths
linkSel.attr('d', (d) => {
const sx = d.source.x, sy = d.source.y;
const tx = d.target.x, ty = d.target.y;
const dx = tx - sx, dy = ty - sy;
const dr = Math.sqrt(dx * dx + dy * dy) * 0.5;
return 'M' + sx + ',' + sy + 'A' + dr + ',' + dr + ' 0 0,1 ' + tx + ',' + ty;
});
// Update node positions
nodeSel.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');
// Update event badge positions (relative to their commit)
eventSel.attr('transform', (d) => {
const commit = nodeData.find((c) => c.id === d.commitId);
return commit ? 'translate(' + commit.x + ',' + commit.y + ')' : 'translate(0,0)';
});
// Update branch label positions
labelSel.attr('transform', (d) => {
const commit = nodeData.find((c) => c.id === d.commitId);
return commit ? 'translate(' + commit.x + ',' + commit.y + ')' : 'translate(0,0)';
});
});
sim.alpha(1).restart();
// --- Highlighting based on selection ---
highlightSelection(state.selectedCommitId, state.selectedEventId);
}
function highlightSelection(commitId, eventId) {
const nodeSel = g.selectAll('.commit-node');
const eventSel = g.selectAll('.event-badge');
const linkSel = g.selectAll('.link-path');
// Reset all
nodeSel.selectAll('.commit-ring').attr('stroke-width', 2).attr('filter', null);
nodeSel.selectAll('.commit-inner').transition().duration(200).attr('r', 12);
eventSel.attr('opacity', (d) => eventId && d.id === eventId ? 1 : 0.4);
if (commitId) {
// Highlight selected commit node
nodeSel.filter((d) => d.id === commitId)
.selectAll('.commit-ring')
.attr('stroke-width', 3)
.attr('filter', 'url(#glow)');
nodeSel.filter((d) => d.id === commitId)
.selectAll('.commit-inner')
.transition().duration(200).attr('r', 14);
// Highlight connected links
linkSel.attr('stroke', (d) => {
if (d.source?.id === commitId || d.target?.id === commitId)
return '#58a6ff';
return '#30363d';
}).attr('opacity', (d) => {
if (d.source?.id === commitId || d.target?.id === commitId)
return 1;
return 0.2;
});
// Highlight events on this commit
eventSel.attr('opacity', (d) => {
if (d.commitId === commitId)
return 1;
return 0.3;
});
}
else {
linkSel.attr('stroke', '#30363d').attr('opacity', 0.6);
eventSel.attr('opacity', (d) => eventId && d.id === eventId ? 1 : 0.7);
}
}
function resizeGraph() {
const container = document.getElementById('graph-container');
width = container.clientWidth;
height = container.clientHeight;
svg.attr('width', '100%').attr('height', '100%');
}
};
// ── module: timeline-view.ts ──
__mods["timeline-view.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderTimeline = renderTimeline;
const state_1 = require("./state");
const types_1 = require("./types");
function renderTimeline() {
const state = (0, state_1.getState)();
const container = document.getElementById('timeline-container');
const commits = state.commits;
const events = state.events;
const entries = [];
for (const c of commits) {
entries.push({ kind: 'commit', date: c.date, commit: c });
}
for (const e of events) {
entries.push({ kind: 'event', date: new Date().toISOString(), event: e });
}
// Sort by date
entries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
// Build HTML
let html = '<div class="timeline-track">';
for (const entry of entries) {
if (entry.kind === 'commit') {
const c = entry.commit;
const isSelected = state.selectedCommitId === c.id;
const hasEvents = events.some(e => e.commitId === c.id);
html += `<div class="timeline-item">`;
html += `<div class="timeline-marker" style="${isSelected ? 'background:#1f6feb;' : ''}" data-commit-id="${c.id}"></div>`;
html += `<div class="timeline-card" data-commit-id="${c.id}">`;
html += `<h4>${escapeHtml(c.message)}</h4>`;
html += `<p><code style="color:#58a6ff">${(0, types_1.shortHash)(c.id)}</code> · ${formatDate(c.date)}`;
if (c.author)
html += ` · by ${escapeHtml(c.author)}`;
if (c.tag)
html += ` <span style="color:#f0883e;font-weight:bold">🏷 ${escapeHtml(c.tag)}</span>`;
html += `</p>`;
if (hasEvents) {
const commitEvents = events.filter(e => e.commitId === c.id);
html += `<div style="margin-top:6px;display:flex;gap:4px;flex-wrap:wrap">`;
for (const ev of commitEvents) {
html += `<span class="event-chip" data-event-id="${ev.id}" title="${escapeHtml(ev.title)}">${types_1.EVENT_ICONS[ev.type]} ${escapeHtml(ev.title)}</span>`;
}
html += `</div>`;
}
html += `</div></div>`;
}
else {
const e = entry.event;
const isSelected = state.selectedEventId === e.id;
html += `<div class="timeline-item">`;
html += `<div class="timeline-marker event-marker" style="${isSelected ? 'background:#a371f7;' : ''}" data-event-id="${e.id}"></div>`;
html += `<div class="timeline-card" data-event-id="${e.id}" style="border-left:3px solid ${getEventColor(e.type)}">`;
html += `<h4>${types_1.EVENT_ICONS[e.type]} ${escapeHtml(e.title)}</h4>`;
html += `<p>${escapeHtml(e.description || '')}</p>`;
const linkedCommit = commits.find(c => c.id === e.commitId);
if (linkedCommit) {
html += `<p style="margin-top:4px">Linked to: <code style="color:#58a6ff">${(0, types_1.shortHash)(linkedCommit.id)}</code> — ${escapeHtml(linkedCommit.message)}</p>`;
}
html += `</div></div>`;
}
}
html += '</div>';
container.innerHTML = html;
// Attach click handlers
container.querySelectorAll('.timeline-card[data-commit-id]').forEach(el => {
el.addEventListener('click', () => {
const id = el.getAttribute('data-commit-id');
(0, state_1.selectCommit)(state.selectedCommitId === id ? undefined : id);
});
});
container.querySelectorAll('.timeline-marker[data-commit-id]').forEach(el => {
el.addEventListener('click', () => {
const id = el.getAttribute('data-commit-id');
(0, state_1.selectCommit)(state.selectedCommitId === id ? undefined : id);
});
});
container.querySelectorAll('[data-event-id]').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
const id = el.getAttribute('data-event-id');
(0, state_1.selectEvent)(state.selectedEventId === id ? undefined : id);
// Also find the linked commit
const evt = events.find(ev => ev.id === id);
if (evt)
(0, state_1.selectCommit)(evt.commitId);
});
});
}
function getEventColor(type) {
const colors = {
milestone: '#a371f7', freeze: '#db6d28', release: '#238636',
review: '#58a6ff', incident: '#f85149', other: '#8b949e',
};
return colors[type] || '#8b949e';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(iso) {
const d = new Date(iso);
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0)
return 'today';
if (diffDays === 1)
return 'yesterday';
if (diffDays < 7)
return diffDays + ' days ago';
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
// Add event chip styles dynamically
if (!document.getElementById('event-chip-style')) {
const style = document.createElement('style');
style.id = 'event-chip-style';
style.textContent = `.event-chip{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:#21262d;border:1px solid #30363d;cursor:pointer;transition:all .15s}.event-chip:hover{background:#30363d;border-color:#8b949e}`;
document.head.appendChild(style);
}
};
// ── module: ui-panels.ts ──
__mods["ui-panels.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setView = setView;
exports.getCurrentView = getCurrentView;
exports.renderSidebar = renderSidebar;
exports.showNewCommitModal = showNewCommitModal;
exports.showEditCommitModal = showEditCommitModal;
exports.showNewBranchModal = showNewBranchModal;
exports.showNewEventModal = showNewEventModal;
exports.showEditEventModal = showEditEventModal;
exports.handleImport = handleImport;
exports.handleExport = handleExport;
const state_1 = require("./state");
const types_1 = require("./types");
let currentView = 'graph';
function setView(view) {
currentView = view;
}
function getCurrentView() {
return currentView;
}
// --- Sidebar Rendering ---
function renderSidebar() {
const state = (0, state_1.getState)();
renderBranchList(state.branches);
renderEventList(state.events);
renderDetailPanel(state);
}
function renderBranchList(branches) {
const ul = document.getElementById('branches-ul');
let html = '';
for (const b of branches) {
html += `<li class="branch-item" data-branch="${escapeHtml(b.name)}">`;
html += `<span class="branch-dot" style="background:${b.color}"></span>`;
html += `<span>${escapeHtml(b.name)}</span>`;
html += `</li>`;
}
ul.innerHTML = html;
ul.querySelectorAll('.branch-item').forEach(el => {
el.addEventListener('click', () => {
const name = el.getAttribute('data-branch');
const branch = branches.find(b => b.name === name);
if (branch)
(0, state_1.selectCommit)(branch.commitId);
});
});
}
function renderEventList(events) {
const ul = document.getElementById('events-ul');
let html = '';
for (const e of events) {
const icons = {
milestone: '🏁', freeze: '❄️', release: '🚀', review: '🔍', incident: '⚠️', other: '📌',
};
html += `<li class="event-item" data-event-id="${e.id}">`;
html += `<span>${icons[e.type] || '📌'}</span>`;
html += `<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(e.title)}</span>`;
html += `</li>`;
}
ul.innerHTML = html;
const currentState = (0, state_1.getState)();
ul.querySelectorAll('.event-item').forEach(el => {
el.addEventListener('click', () => {
const id = el.getAttribute('data-event-id');
(0, state_1.selectEvent)(currentState.selectedEventId === id ? undefined : id);
const evt = events.find(e => e.id === id);
if (evt)
(0, state_1.selectCommit)(evt.commitId);
});
});
}
function renderDetailPanel(state) {
const content = document.getElementById('detail-content');
// Check for selected event first
if (state.selectedEventId) {
const evt = state.events.find(e => e.id === state.selectedEventId);
if (evt) {
const commit = state.commits.find(c => c.id === evt.commitId);
let html = '<div style="border-left:3px solid #a371f7;padding-left:12px">';
html += `<h4 style="margin-bottom:8px">${getEventIcon(evt.type)} ${escapeHtml(evt.title)}</h4>`;
html += detailRow('Type', evt.type);
html += detailRow('Description', evt.description || '—');
html += detailRow('Linked Commit', commit ? `${(0, types_1.shortHash)(commit.id)} — ${escapeHtml(commit.message)}` : 'Unknown');
html += `<button class="tool-btn" style="margin-top:8px;font-size:12px" onclick="window.__editEvent('${evt.id}')">✏️ Edit</button> `;
html += `<button class="tool-btn danger" style="margin-top:8px;font-size:12px" onclick="window.__deleteEvent('${evt.id}')">🗑 Delete</button>`;
html += '</div>';
content.innerHTML = html;
return;
}
}
// Check for selected commit
if (state.selectedCommitId) {
const commit = state.commits.find(c => c.id === state.selectedCommitId);
if (commit) {
const branchesHere = state.branches.filter(b => b.commitId === commit.id);
const eventsHere = state.events.filter(e => e.commitId === commit.id);
let html = `<h4 style="margin-bottom:8px;font-family:monospace;color:#58a6ff">${(0, types_1.shortHash)(commit.id)}</h4>`;
html += detailRow('Message', commit.message);
html += detailRow('Author', commit.author || '—');
html += detailRow('Date', new Date(commit.date).toLocaleString());
if (commit.tag)
html += detailRow('Tag', commit.tag);
html += detailRow('Parents', commit.parents.length > 0 ? commit.parents.map(p => (0, types_1.shortHash)(p)).join(', ') : '—');
if (branchesHere.length > 0) {
html += `<div class="detail-row"><span class="detail-label">Branches</span>`;
for (const b of branchesHere) {
html += `<span style="display:inline-block;margin:2px;padding:2px 8px;border-radius:10px;font-size:11px;background:${b.color}33;color:${b.color};border:1px solid ${b.color}">${escapeHtml(b.name)}</span>`;
}
html += `</div>`;
}
if (eventsHere.length > 0) {
html += `<div class="detail-row"><span class="detail-label">Events (${eventsHere.length})</span>`;
for (const e of eventsHere) {
html += `<div style="padding:4px 8px;margin:2px 0;border-radius:6px;background:#1c2129;font-size:12px;cursor:pointer" data-event-id="${e.id}">${getEventIcon(e.type)} ${escapeHtml(e.title)}</div>`;
}
html += `</div>`;
}
html += `<button class="tool-btn" style="margin-top:8px;font-size:12px" onclick="window.__editCommit('${commit.id}')">✏️ Edit</button> `;
html += `<button class="tool-btn danger" style="margin-top:8px;font-size:12px" onclick="window.__deleteCommit('${commit.id}')">🗑 Delete</button>`;
content.innerHTML = html;
// Click on event chips in detail panel
content.querySelectorAll('[data-event-id]').forEach(el => {
el.addEventListener('click', () => {
const id = el.getAttribute('data-event-id');
(0, state_1.selectEvent)(id);
});
});
return;
}
}
// No selection
content.innerHTML = '<p class="hint">Select a commit or event to view details.</p>';
}
function detailRow(label, value) {
return `<div class="detail-row"><span class="detail-label">${label}</span><span class="detail-value">${escapeHtml(value)}</span></div>`;
}
function getEventIcon(type) {
const icons = {
milestone: '🏁', freeze: '❄️', release: '🚀', review: '🔍', incident: '⚠️', other: '📌',
};
return icons[type] || '📌';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- Modal System ---
const overlay = document.getElementById('modal-overlay');
const modalTitle = document.getElementById('modal-title');
const modalBody = document.getElementById('modal-body');
const modalSave = document.getElementById('modal-save');
const modalCancel = document.getElementById('modal-cancel');
const modalClose = document.getElementById('modal-close');
let onSave = null;
function openModal(title, bodyHtml, saveFn) {
modalTitle.textContent = title;
modalBody.innerHTML = bodyHtml;
overlay.style.display = 'flex';
onSave = saveFn || null;
}
function closeModal() {
overlay.style.display = 'none';
onSave = null;
}
modalSave.addEventListener('click', () => {
if (onSave)
onSave();
closeModal();
});
modalCancel.addEventListener('click', closeModal);
modalClose.addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => {
if (e.target === overlay)
closeModal();
});
// --- Commit Modal ---
function showNewCommitModal() {
const state = (0, state_1.getState)();
let parentOptions = '<option value="">(root commit)</option>';
for (const c of state.commits) {
parentOptions += `<option value="${c.id}">${(0, types_1.shortHash)(c.id)} — ${escapeHtml(c.message)}</option>`;
}
const bodyHtml = `
<div class="form-group"><label>Message</label><input id="f-commit-msg" type="text" placeholder="Commit message..." autofocus></div>
<div class="form-group"><label>Author</label><input id="f-commit-author" type="text" placeholder="Author name..."></div>
<div class="form-group"><label>Parent Commit(s)</label><select id="f-commit-parents" multiple size="4">${parentOptions}</select></div>
<div class="form-group"><label>Date (ISO, optional)</label><input id="f-commit-date" type="text" placeholder="${new Date().toISOString()}"></div>
<div class="form-group"><label>Tag (optional)</label><input id="f-commit-tag" type="text" placeholder="e.g. v1.0"></div>
`;
openModal('New Commit', bodyHtml, () => {
const msg = document.getElementById('f-commit-msg').value.trim();
if (!msg)
return alert('Message is required');
const author = document.getElementById('f-commit-author').value.trim() || undefined;
const dateInput = document.getElementById('f-commit-date').value.trim();
const tag = document.getElementById('f-commit-tag').value.trim() || undefined;
const sel = document.getElementById('f-commit-parents');
const parents = [];
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].selected && sel.options[i].value)
parents.push(sel.options[i].value);
}
const newCommit = { message: msg, parents };
if (author)
newCommit.author = author;
if (tag)
newCommit.tag = tag;
if (dateInput)
newCommit.date = dateInput;
(0, state_1.addCommit)(newCommit);
});
}
function showEditCommitModal(id) {
const state = (0, state_1.getState)();
const commit = state.commits.find(c => c.id === id);
if (!commit)
return;
let parentOptions = '<option value="">(none)</option>';
for (const c of state.commits) {
if (c.id !== id) {
const selected = commit.parents.includes(c.id) ? 'selected' : '';
parentOptions += `<option value="${c.id}" ${selected}>${(0, types_1.shortHash)(c.id)} — ${escapeHtml(c.message)}</option>`;
}
}
const bodyHtml = `
<div class="form-group"><label>Message</label><input id="f-commit-msg" type="text" value="${escapeAttr(commit.message)}"></div>
<div class="form-group"><label>Author</label><input id="f-commit-author" type="text" value="${escapeAttr(commit.author || '')}"></div>
<div class="form-group"><label>Parent Commit(s)</label><select id="f-commit-parents" multiple size="4">${parentOptions}</select></div>
<div class="form-group"><label>Date (ISO)</label><input id="f-commit-date" type="text" value="${escapeAttr(commit.date)}"></div>
<div class="form-group"><label>Tag</label><input id="f-commit-tag" type="text" value="${escapeAttr(commit.tag || '')}"></div>
`;
openModal('Edit Commit', bodyHtml, () => {
const msg = document.getElementById('f-commit-msg').value.trim();
if (!msg)
return alert('Message is required');
const author = document.getElementById('f-commit-author').value.trim() || undefined;
const dateInput = document.getElementById('f-commit-date').value.trim();
const tag = document.getElementById('f-commit-tag').value.trim() || undefined;
const sel = document.getElementById('f-commit-parents');
const parents = [];
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].selected && sel.options[i].value)
parents.push(sel.options[i].value);
}
const updates = { message: msg, parents };
if (author)
updates.author = author;
if (dateInput)
updates.date = dateInput;
if (tag)
updates.tag = tag;
(0, state_1.updateCommit)(id, updates);
});
}
// --- Branch Modal ---
function showNewBranchModal() {
const state = (0, state_1.getState)();
let options = '';
for (const c of state.commits) {
options += `<option value="${c.id}">${(0, types_1.shortHash)(c.id)} — ${escapeHtml(c.message)}</option>`;
}
const bodyHtml = `
<div class="form-group"><label>Branch Name</label><input id="f-branch-name" type="text" placeholder="e.g. feature/new-ui" autofocus></div>
<div class="form-group"><label>Points to Commit</label><select id="f-branch-commit">${options}</select></div>
`;
openModal('New Branch', bodyHtml, () => {
const name = document.getElementById('f-branch-name').value.trim();
if (!name)
return alert('Branch name is required');
const commitId = document.getElementById('f-branch-commit').value;
(0, state_1.addBranch)(name, commitId);
});
}
// --- Event Modal ---
function showNewEventModal() {
const state = (0, state_1.getState)();
let options = '';
for (const c of state.commits) {
options += `<option value="${c.id}">${(0, types_1.shortHash)(c.id)} — ${escapeHtml(c.message)}</option>`;
}
const bodyHtml = `
<div class="form-group"><label>Title</label><input id="f-event-title" type="text" placeholder="e.g. Code Freeze" autofocus></div>
<div class="form-group"><label>Description</label><textarea id="f-event-desc" placeholder="Description..."></textarea></div>
<div class="form-group"><label>Type</label><select id="f-event-type">
<option value="milestone">🏁 Milestone</option>
<option value="freeze">❄️ Code Freeze</option>
<option value="release">🚀 Release</option>
<option value="review">🔍 Review</option>
<option value="incident">⚠️ Incident</option>
<option value="other">📌 Other</option>
</select></div>
<div class="form-group"><label>Link to Commit (required)</label><select id="f-event-commit">${options}</select></div>
`;
openModal('New Event', bodyHtml, () => {
const title = document.getElementById('f-event-title').value.trim();
if (!title)
return alert('Title is required');
const description = document.getElementById('f-event-desc').value.trim() || undefined;
const type = document.getElementById('f-event-type').value;
const commitId = document.getElementById('f-event-commit').value;
(0, state_1.addEvent)({ title, description, type, commitId });
});
}
function showEditEventModal(id) {
const state = (0, state_1.getState)();
const evt = state.events.find(e => e.id === id);
if (!evt)
return;
let options = '';
for (const c of state.commits) {
const selected = c.id === evt.commitId ? 'selected' : '';
options += `<option value="${c.id}" ${selected}>${(0, types_1.shortHash)(c.id)} — ${escapeHtml(c.message)}</option>`;
}
const bodyHtml = `
<div class="form-group"><label>Title</label><input id="f-event-title" type="text" value="${escapeAttr(evt.title)}"></div>
<div class="form-group"><label>Description</label><textarea id="f-event-desc">${escapeHtml(evt.description || '')}</textarea></div>
<div class="form-group"><label>Type</label><select id="f-event-type">
${['milestone', 'freeze', 'release', 'review', 'incident', 'other'].map(t => `<option value="${t}" ${t === evt.type ? 'selected' : ''}>${getEventIcon(t)} ${t}</option>`).join('')}
</select></div>
<div class="form-group"><label>Link to Commit</label><select id="f-event-commit">${options}</select></div>
`;
openModal('Edit Event', bodyHtml, () => {
const title = document.getElementById('f-event-title').value.trim();
if (!title)
return alert('Title is required');
const description = document.getElementById('f-event-desc').value.trim() || undefined;
const type = document.getElementById('f-event-type').value;
const commitId = document.getElementById('f-event-commit').value;
(0, state_1.updateEvent)(id, { title, description, type, commitId });
});
}
function escapeAttr(text) {
return text.replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
}
window.__editCommit = showEditCommitModal;
window.__deleteCommit = (id) => { if (confirm('Delete this commit and its linked events?'))
(0, state_1.deleteCommit)(id); };
window.__editEvent = showEditEventModal;
window.__deleteEvent = (id) => { if (confirm('Delete this event?'))
(0, state_1.deleteEvent)(id); };
// --- Import/Export ---
function handleImport(file) {
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
if (data.commits && Array.isArray(data.commits)) {
(0, state_1.importState)(data);
}
else {
alert('Invalid file format. Expected a JSON with "commits" array.');
}
}
catch {
alert('Failed to parse JSON file.');
}
};
reader.readAsText(file);
}
function handleExport() {
const state = (0, state_1.getState)();
const json = JSON.stringify(state, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'git-visualizer-data.json';
a.click();
URL.revokeObjectURL(url);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"a92efb1c-af68-4361-9991-0060b44c20dd","tokensIn":2551214,"tokensOut":39059,"tokensTotal":2590273,"turns":57,"toolCalls":56,"failedToolCalls":3,"timestamp":"2026-07-30T21:48:19.682Z"} -->