← Git Explorer results

Git Explorer

Qwen 3.6 27b MTP · typescript

Initial view of Git Explorer

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 &amp; 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, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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"} -->