/** * FlowGit Landing Page Interactive Logic * Axiom Design System & Blueprint DAG Engine */ (function () { 'use strict'; // ── Global State ── const state = { activeTab: 'graph', zoom: 1, pan: { x: 40, y: 40 }, isDragging: false, dragStart: { x: 0, y: 0 }, selectedNodeId: 'c1', diffMode: 'unified', commandTab: 'portable', stagedFiles: [ { id: 'f1', name: 'electron/git/executor.ts', status: 'M', additions: 34, deletions: 2 }, { id: 'f2', name: 'src/views/Repository/Graph/GraphView.tsx', status: 'A', additions: 188, deletions: 0 }, ], unstagedFiles: [ { id: 'f3', name: 'shared/types/git.types.ts', status: 'M', additions: 12, deletions: 4 }, { id: 'f4', name: 'DESIGN.md', status: 'M', additions: 6, deletions: 1 }, { id: 'f5', name: 'tests/git/executor.test.ts', status: 'U', additions: 45, deletions: 0 }, ] }; // ── Blueprint Commit Graph Topology ── const graphNodes = [ { id: 'c1', hash: 'a94f1c0', shortHash: 'a94f1c0', branch: 'main', isHead: true, tags: ['HEAD -> main', 'origin/main'], color: '#da5c2c', x: 640, y: 100, author: 'Alex Vance ', date: '2026-10-25 22:41:18', message: 'feat(graph): topological DAG bezier connector & minimap sync', parentIds: ['c2', 'c3'], files: [ { path: 'src/views/Repository/Graph/GraphView.tsx', status: 'M', adds: 84, dels: 12 }, { path: 'electron/git/history.ts', status: 'M', adds: 42, dels: 5 } ], diffSnippet: `@@ -118,9 +118,15 @@ export function computeBezierCurves(nodes, edges) { - const path = \`M \${source.x} \${source.y} L \${target.x} \${target.y}\`; + const dx = Math.abs(target.x - source.x) * 0.5; + const cp1x = source.x + dx; + const cp2x = target.x - dx; + const path = \`M \${source.x} \${source.y} C \${cp1x} \${source.y}, \${cp2x} \${target.y}, \${target.x} \${target.y}\`; + return { id: \`\${source.id}-\${target.id}\`, d: path, color: edgeColor }; }` }, { id: 'c2', hash: '7b3e9a1', shortHash: '7b3e9a1', branch: 'main', isHead: false, tags: [], color: '#da5c2c', x: 360, y: 100, author: 'Alex Vance ', date: '2026-10-25 19:14:02', message: 'perf(sqlite): WAL mode persistence for 100k+ commit repositories', parentIds: ['c4'], files: [ { path: 'electron/persistence/database.ts', status: 'M', adds: 26, dels: 3 } ], diffSnippet: `@@ -42,3 +42,7 @@ export function initDatabase(dbPath: string) { + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.pragma('cache_size = -64000'); // 64MB memory cache` }, { id: 'c3', hash: 'f419c82', shortHash: 'f419c82', branch: 'feature/blueprint-minimap', isHead: false, tags: ['feat/minimap'], color: '#38bdf8', x: 360, y: 260, author: 'Elena Rostova ', date: '2026-10-25 18:02:40', message: 'feat(minimap): radar viewport bounding box with pan listener', parentIds: ['c4'], files: [ { path: 'src/components/MinimapRadar.tsx', status: 'A', adds: 112, dels: 0 } ], diffSnippet: `@@ -0,0 +1,18 @@ +export const MinimapRadar: React.FC = ({ bounds, viewport }) => { + const ratioX = 140 / bounds.width; + const ratioY = 90 / bounds.height; + return
; +};` }, { id: 'c4', hash: 'd28a011', shortHash: 'd28a011', branch: 'main', isHead: false, tags: ['v1.0.0-rc.2'], color: '#da5c2c', x: 80, y: 100, author: 'Alex Vance ', date: '2026-10-24 23:55:10', message: 'security(executor): enforce argv array spawning with safe env redaction', parentIds: [], files: [ { path: 'electron/git/executor.ts', status: 'M', adds: 65, dels: 18 } ], diffSnippet: `@@ -15,7 +15,9 @@ export function executeGit(args: string[], cwd: string) { - // DEPRECATED: exec(\`git \${args.join(' ')}\`) + // Safe argv spawn — no shell string interpolation + return spawn(gitPath, args, { cwd, shell: false, env: sanitizedEnv }); }` } ]; // ── Initialization ── document.addEventListener('DOMContentLoaded', () => { initAnnouncement(); initTabs(); initBlueprintGraph(); initStagingDemo(); initDiffViewer(); initHistogram(); initTerminalTabs(); initClipboardCopy(); initKeyboardShortcuts(); initRepoLinks(); updateDiffDisplay(); }); // ── 1. Announcement Banner ── function initAnnouncement() { const banner = document.getElementById('announcementBanner'); const closeBtn = document.getElementById('closeAnnouncement'); if (!banner || !closeBtn) return; if (sessionStorage.getItem('flowgit_announcement_closed') === 'true') { banner.style.display = 'none'; } closeBtn.addEventListener('click', () => { banner.style.display = 'none'; sessionStorage.setItem('flowgit_announcement_closed', 'true'); }); } // ── 2. Showcase Navigation Tabs ── function initTabs() { const tabs = document.querySelectorAll('.product-tab'); const panels = document.querySelectorAll('.view-panel'); tabs.forEach((tab) => { tab.addEventListener('click', () => { const target = tab.getAttribute('data-tab'); if (!target) return; tabs.forEach((t) => t.classList.remove('active')); panels.forEach((p) => p.classList.remove('active')); tab.classList.add('active'); const activePanel = document.getElementById(`panel-${target}`); if (activePanel) { activePanel.classList.add('active'); } state.activeTab = target; // If graph tab opened, recalculate center if (target === 'graph') { renderGraph(); } }); }); } // ── 3. Blueprint DAG SVG Engine ── function initBlueprintGraph() { const container = document.getElementById('graphCanvasContainer'); const svg = document.getElementById('graphSvg'); if (!container || !svg) return; // Zoom Controls const zoomInBtn = document.getElementById('zoomInBtn'); const zoomOutBtn = document.getElementById('zoomOutBtn'); const zoomFitBtn = document.getElementById('zoomFitBtn'); const jumpHeadBtn = document.getElementById('jumpHeadBtn'); const searchInput = document.getElementById('graphSearchInput'); if (zoomInBtn) { zoomInBtn.addEventListener('click', () => { state.zoom = Math.min(2.5, state.zoom + 0.15); renderGraph(); }); } if (zoomOutBtn) { zoomOutBtn.addEventListener('click', () => { state.zoom = Math.max(0.4, state.zoom - 0.15); renderGraph(); }); } if (zoomFitBtn) { zoomFitBtn.addEventListener('click', () => { fitGraphToView(); }); } if (jumpHeadBtn) { jumpHeadBtn.addEventListener('click', () => { jumpToHead(); }); } if (searchInput) { searchInput.addEventListener('input', (e) => { const query = e.target.value.toLowerCase().trim(); filterGraphNodes(query); }); } // Mouse Pan & Drag container.addEventListener('mousedown', (e) => { // Ignore click on node cards or controls if (e.target.closest('.node-card-group') || e.target.closest('.graph-controls-toolbar')) { return; } state.isDragging = true; state.dragStart = { x: e.clientX - state.pan.x, y: e.clientY - state.pan.y }; container.style.cursor = 'grabbing'; }); window.addEventListener('mousemove', (e) => { if (!state.isDragging) return; state.pan.x = e.clientX - state.dragStart.x; state.pan.y = e.clientY - state.dragStart.y; renderGraph(); }); window.addEventListener('mouseup', () => { if (state.isDragging) { state.isDragging = false; container.style.cursor = 'grab'; } }); // Mouse wheel zoom container.addEventListener('wheel', (e) => { e.preventDefault(); const zoomFactor = e.deltaY < 0 ? 1.08 : 0.92; const newZoom = Math.max(0.4, Math.min(2.5, state.zoom * zoomFactor)); // Zoom centered on cursor const rect = container.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; state.pan.x = mouseX - (mouseX - state.pan.x) * (newZoom / state.zoom); state.pan.y = mouseY - (mouseY - state.pan.y) * (newZoom / state.zoom); state.zoom = newZoom; renderGraph(); }, { passive: false }); // Close Inspector button const closeInspectorBtn = document.getElementById('closeInspectorBtn'); if (closeInspectorBtn) { closeInspectorBtn.addEventListener('click', () => { const drawer = document.getElementById('commitInspector'); if (drawer) drawer.classList.remove('open'); }); } renderGraph(); } function renderGraph() { const svg = document.getElementById('graphSvg'); if (!svg) return; const NODE_WIDTH = 230; const NODE_HEIGHT = 76; // Clear previous SVG content svg.innerHTML = ''; // Create main viewport transformation group const gViewport = document.createElementNS('http://www.w3.org/2000/svg', 'g'); gViewport.setAttribute('transform', `translate(${state.pan.x}, ${state.pan.y}) scale(${state.zoom})`); // 1. Draw Bezier Connector Edges graphNodes.forEach((node) => { node.parentIds.forEach((parentId) => { const parent = graphNodes.find((n) => n.id === parentId); if (!parent) return; // Source = parent right edge; Target = child left edge const startX = parent.x + NODE_WIDTH; const startY = parent.y + NODE_HEIGHT / 2; const endX = node.x; const endY = node.y + NODE_HEIGHT / 2; const dx = Math.max(40, Math.abs(endX - startX) * 0.5); const cp1x = startX + dx; const cp1y = startY; const cp2x = endX - dx; const cp2y = endY; const pathData = `M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', pathData); path.setAttribute('class', 'dag-edge'); path.setAttribute('stroke', node.color); path.setAttribute('stroke-width', '2'); path.setAttribute('stroke-opacity', '0.7'); gViewport.appendChild(path); }); }); // 2. Draw Commit Nodes graphNodes.forEach((node) => { const isSelected = state.selectedNodeId === node.id; const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); group.setAttribute('class', `node-card-group ${isSelected ? 'selected' : ''}`); group.setAttribute('data-node-id', node.id); group.setAttribute('transform', `translate(${node.x}, ${node.y})`); group.style.cursor = 'pointer'; // Base card rectangle const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('width', NODE_WIDTH); rect.setAttribute('height', NODE_HEIGHT); rect.setAttribute('rx', '2'); rect.setAttribute('fill', '#111111'); rect.setAttribute('stroke', isSelected ? '#da5c2c' : '#202020'); rect.setAttribute('stroke-width', isSelected ? '2' : '1'); group.appendChild(rect); // Branch color indicator stripe on left edge (2px) const stripe = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); stripe.setAttribute('x', '0'); stripe.setAttribute('y', '0'); stripe.setAttribute('width', '3'); stripe.setAttribute('height', NODE_HEIGHT); stripe.setAttribute('rx', '1'); stripe.setAttribute('fill', node.color); group.appendChild(stripe); // Short Hash Tag Pill const hashText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); hashText.setAttribute('x', '12'); hashText.setAttribute('y', '22'); hashText.setAttribute('fill', '#eeeeee'); hashText.setAttribute('font-family', 'var(--font-mono)'); hashText.setAttribute('font-size', '12'); hashText.setAttribute('font-weight', '700'); hashText.textContent = node.shortHash; group.appendChild(hashText); // Branch / Tag Badge (if any) if (node.tags.length > 0) { const badgeBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); badgeBg.setAttribute('x', '76'); badgeBg.setAttribute('y', '10'); badgeBg.setAttribute('width', '140'); badgeBg.setAttribute('height', '16'); badgeBg.setAttribute('rx', '2'); badgeBg.setAttribute('fill', '#191919'); badgeBg.setAttribute('stroke', '#3a3a3a'); group.appendChild(badgeBg); const badgeText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); badgeText.setAttribute('x', '82'); badgeText.setAttribute('y', '22'); badgeText.setAttribute('fill', node.color); badgeText.setAttribute('font-family', 'var(--font-mono)'); badgeText.setAttribute('font-size', '10'); badgeText.setAttribute('font-weight', '700'); badgeText.textContent = node.tags.join(', ').substring(0, 22); group.appendChild(badgeText); } // Commit Message const msgText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); msgText.setAttribute('x', '12'); msgText.setAttribute('y', '44'); msgText.setAttribute('fill', '#b4b4b4'); msgText.setAttribute('font-family', 'var(--font-mono)'); msgText.setAttribute('font-size', '11'); const truncated = node.message.length > 28 ? node.message.substring(0, 27) + '…' : node.message; msgText.textContent = truncated; group.appendChild(msgText); // Author & Time Footer const authorText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); authorText.setAttribute('x', '12'); authorText.setAttribute('y', '62'); authorText.setAttribute('fill', '#606060'); authorText.setAttribute('font-family', 'var(--font-mono)'); authorText.setAttribute('font-size', '10'); authorText.textContent = node.author.split('<')[0].trim() + ' • ' + node.date.split(' ')[0]; group.appendChild(authorText); // Click event on node group.addEventListener('click', (e) => { e.stopPropagation(); selectNode(node); }); gViewport.appendChild(group); }); svg.appendChild(gViewport); updateMinimap(); } function selectNode(node) { state.selectedNodeId = node.id; renderGraph(); // Populate Inspector Drawer const drawer = document.getElementById('commitInspector'); if (!drawer) return; document.getElementById('inspHash').textContent = node.hash; document.getElementById('inspAuthor').textContent = node.author; document.getElementById('inspDate').textContent = node.date; document.getElementById('inspMessage').textContent = node.message; // Populate files const fileListEl = document.getElementById('inspFileList'); if (fileListEl) { fileListEl.innerHTML = node.files .map( (f) => `
${f.path} +${f.adds} -${f.dels}
` ) .join(''); } const diffPreviewEl = document.getElementById('inspDiffCode'); if (diffPreviewEl) { diffPreviewEl.textContent = node.diffSnippet; } drawer.classList.add('open'); } function jumpToHead() { const headNode = graphNodes.find((n) => n.isHead); if (!headNode) return; const container = document.getElementById('graphCanvasContainer'); if (!container) return; const w = container.clientWidth; const h = container.clientHeight; state.pan.x = w / 2 - (headNode.x + 115) * state.zoom; state.pan.y = h / 2 - (headNode.y + 38) * state.zoom; renderGraph(); } function fitGraphToView() { const container = document.getElementById('graphCanvasContainer'); if (!container) return; state.zoom = 0.9; state.pan.x = 40; state.pan.y = 80; renderGraph(); } function filterGraphNodes(query) { const groups = document.querySelectorAll('.node-card-group'); groups.forEach((g) => { const nodeId = g.getAttribute('data-node-id'); const node = graphNodes.find((n) => n.id === nodeId); if (!node) return; if (!query || node.message.toLowerCase().includes(query) || node.hash.includes(query)) { g.style.opacity = '1'; } else { g.style.opacity = '0.25'; } }); } function updateMinimap() { const box = document.getElementById('minimapBox'); if (!box) return; // Compute relative viewport position const normX = Math.max(0, Math.min(100, (-state.pan.x / 10) + 20)); const normY = Math.max(0, Math.min(60, (-state.pan.y / 10) + 10)); box.style.left = `${normX}px`; box.style.top = `${normY}px`; box.style.width = `${Math.max(30, 80 / state.zoom)}px`; box.style.height = `${Math.max(20, 50 / state.zoom)}px`; } // ── 4. Interactive Staging Demo (Tab 2) ── function initStagingDemo() { renderStagingLists(); const commitInput = document.getElementById('commitInputMsg'); const charCounter = document.getElementById('commitCharCounter'); const commitBtn = document.getElementById('commitBtn'); if (commitInput && charCounter) { commitInput.addEventListener('input', () => { const len = commitInput.value.length; charCounter.textContent = `${len} / 72 chars`; if (len > 72) { charCounter.style.color = '#ef4444'; } else { charCounter.style.color = '#606060'; } }); } if (commitBtn && commitInput) { commitBtn.addEventListener('click', () => { if (!commitInput.value.trim()) { showToast('Please enter a commit message'); return; } if (state.stagedFiles.length === 0) { showToast('No changes staged to commit'); return; } const msg = commitInput.value.trim(); showToast(`[OK] Committed: "${msg.substring(0, 30)}..."`); state.stagedFiles = []; commitInput.value = ''; if (charCounter) charCounter.textContent = '0 / 72 chars'; renderStagingLists(); }); } const stageAllBtn = document.getElementById('stageAllBtn'); if (stageAllBtn) { stageAllBtn.addEventListener('click', () => { state.stagedFiles = [...state.stagedFiles, ...state.unstagedFiles]; state.unstagedFiles = []; renderStagingLists(); showToast('All files staged (porcelain index updated)'); }); } const unstageAllBtn = document.getElementById('unstageAllBtn'); if (unstageAllBtn) { unstageAllBtn.addEventListener('click', () => { state.unstagedFiles = [...state.unstagedFiles, ...state.stagedFiles]; state.stagedFiles = []; renderStagingLists(); showToast('All files unstaged'); }); } } function renderStagingLists() { const stagedEl = document.getElementById('stagedFilesContainer'); const unstagedEl = document.getElementById('unstagedFilesContainer'); const stagedCountEl = document.getElementById('stagedCount'); const unstagedCountEl = document.getElementById('unstagedCount'); if (stagedCountEl) stagedCountEl.textContent = `(${state.stagedFiles.length})`; if (unstagedCountEl) unstagedCountEl.textContent = `(${state.unstagedFiles.length})`; if (stagedEl) { stagedEl.innerHTML = state.stagedFiles.length === 0 ? '
No files staged in index.
' : state.stagedFiles.map((file) => `
${file.status} ${file.name}
`).join(''); } if (unstagedEl) { unstagedEl.innerHTML = state.unstagedFiles.length === 0 ? '
Working tree clean.
' : state.unstagedFiles.map((file) => `
${file.status} ${file.name}
`).join(''); } // Attach click handlers document.querySelectorAll('.staging-action-btn').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); const action = btn.getAttribute('data-action'); const id = btn.getAttribute('data-id'); if (action === 'stage') { const item = state.unstagedFiles.find((f) => f.id === id); if (item) { state.unstagedFiles = state.unstagedFiles.filter((f) => f.id !== id); state.stagedFiles.push(item); } } else if (action === 'unstage') { const item = state.stagedFiles.find((f) => f.id === id); if (item) { state.stagedFiles = state.stagedFiles.filter((f) => f.id !== id); state.unstagedFiles.push(item); } } renderStagingLists(); }); }); } // ── 5. Advanced Diff Viewer (Tab 3) ── function initDiffViewer() { const unifiedBtn = document.getElementById('diffUnifiedBtn'); const splitBtn = document.getElementById('diffSplitBtn'); if (unifiedBtn && splitBtn) { unifiedBtn.addEventListener('click', () => { state.diffMode = 'unified'; unifiedBtn.classList.add('active'); splitBtn.classList.remove('active'); updateDiffDisplay(); }); splitBtn.addEventListener('click', () => { state.diffMode = 'split'; splitBtn.classList.add('active'); unifiedBtn.classList.remove('active'); updateDiffDisplay(); }); } } function updateDiffDisplay() { const diffContainer = document.getElementById('diffLinesContainer'); if (!diffContainer) return; const diffLines = [ { type: 'header', text: '@@ -45,14 +45,21 @@ export async function executeGitSafe(args, options) {' }, { type: 'context', oldNum: '45', newNum: '45', text: ' const binaryPath = await detectSystemGitBinary();' }, { type: 'context', oldNum: '46', newNum: '46', text: ' if (!binaryPath) {' }, { type: 'context', oldNum: '47', newNum: '47', text: ' throw new GitBinaryNotFoundError();' }, { type: 'context', oldNum: '48', newNum: '48', text: ' }' }, { type: 'deleted', oldNum: '49', newNum: '', text: '- const rawOutput = child_process.execSync(`git ${args.join(" ")}`);' }, { type: 'added', oldNum: '', newNum: '49', text: '+ // SECURITY: Spawn directly with argv arguments array (no shell concatenation)' }, { type: 'added', oldNum: '', newNum: '50', text: '+ const sanitizedArgs = redactAuthenticationTokens(args);' }, { type: 'added', oldNum: '', newNum: '51', text: '+ const child = child_process.spawn(binaryPath, sanitizedArgs, {' }, { type: 'added', oldNum: '', newNum: '52', text: '+ cwd: options.workingDirectory,' }, { type: 'added', oldNum: '', newNum: '53', text: '+ shell: false,' }, { type: 'added', oldNum: '', newNum: '54', text: '+ windowsHide: true,' }, { type: 'added', oldNum: '', newNum: '55', text: '+ });' }, { type: 'context', oldNum: '50', newNum: '56', text: ' return parsePorcelainOutput(child);' }, { type: 'context', oldNum: '51', newNum: '57', text: '}' } ]; if (state.diffMode === 'unified') { diffContainer.innerHTML = diffLines .map((line) => { if (line.type === 'header') { return `
...
${line.text}
`; } const cls = line.type === 'added' ? 'added' : line.type === 'deleted' ? 'deleted' : ''; return `
${line.oldNum || ''}
${line.newNum || ''}
${escapeHtml(line.text)}
`; }) .join(''); } else { // Split mode simulation diffContainer.innerHTML = `
BASE (REVISION A)
45
const binaryPath = await detectSystemGitBinary();
46
if (!binaryPath) {
47
throw new GitBinaryNotFoundError();
48
}
49
- const rawOutput = child_process.execSync(...);
50
return parsePorcelainOutput(child);
HEAD (REVISION B)
45
const binaryPath = await detectSystemGitBinary();
46
if (!binaryPath) {
47
throw new GitBinaryNotFoundError();
48
}
49
+ const child = child_process.spawn(binaryPath...);
50
+ return parsePorcelainOutput(child);
`; } } // ── 6. Activity Histogram (Axiom Design Specification) ── function initHistogram() { const container = document.getElementById('histogramBars'); if (!container) return; // Pattern of 48 activity bars const heights = [ 4, 6, 8, 4, 12, 16, 8, 4, 14, 18, 16, 20, 10, 6, 4, 8, 14, 16, 12, 18, 22, 14, 10, 8, 4, 12, 16, 18, 20, 24, 16, 12, 8, 14, 18, 16, 20, 24, 28, 18, 14, 10, 6, 12, 16, 20, 24, 18 ]; container.innerHTML = heights .map((h, i) => { const isAccent = h >= 22; return `
`; }) .join(''); } // ── 7. Interactive Terminal Commands Switcher ── function initTerminalTabs() { const tabs = document.querySelectorAll('.terminal-tab-btn'); const codeBlock = document.getElementById('terminalCodeDisplay'); if (!tabs || !codeBlock) return; const commands = { portable: `# Standalone Single-File Executable # Download FlowGit-Portable.exe from GitLab (Recommended) or GitHub Mirror: .\\FlowGit-Portable.exe # Zero installation, zero registry writes`, binaries: `# Compiled Binaries (Pre-compiled Unpacked Archive) # 1. Download and extract FlowGit-win-x64-binaries.zip from GitLab / GitHub: Expand-Archive -Path .\\FlowGit-win-x64-binaries.zip -DestinationPath .\\FlowGit # 2. Run the executable directly (no installer required): .\\FlowGit\\FlowGit.exe`, source: `# 1. Clone repository from GitLab (or GitHub Mirror) git clone https://kazaamo.space/djjdjdbf9/flowgit.git cd flowgit # 2. Install dependencies & run dev server npm install npm run dev # 3. Compile standalone Windows directory or portable exe npm run dist:dir npx electron-builder --win portable` }; tabs.forEach((tab) => { tab.addEventListener('click', () => { const type = tab.getAttribute('data-cmd'); if (!type || !commands[type]) return; tabs.forEach((t) => t.classList.remove('active')); tab.classList.add('active'); codeBlock.textContent = commands[type]; state.commandTab = type; }); }); } // ── 8. Clipboard Copy Functionality ── function initClipboardCopy() { const copyBtns = document.querySelectorAll('[data-copy-target]'); copyBtns.forEach((btn) => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-copy-target'); const targetEl = document.getElementById(targetId); if (!targetEl) return; const text = targetEl.textContent || targetEl.innerText; navigator.clipboard.writeText(text.trim()).then(() => { const original = btn.textContent; btn.textContent = '✓ COPIED'; showToast('Copied to clipboard'); setTimeout(() => { btn.textContent = original; }, 1800); }).catch(() => { showToast('Error copying to clipboard'); }); }); }); const copyFloatingBtn = document.getElementById('copyTerminalFloatingBtn'); if (copyFloatingBtn) { copyFloatingBtn.addEventListener('click', () => { const codeEl = document.getElementById('terminalCodeDisplay'); if (!codeEl) return; navigator.clipboard.writeText(codeEl.textContent.trim()).then(() => { copyFloatingBtn.textContent = '✓ COPIED'; showToast('Terminal command copied'); setTimeout(() => { copyFloatingBtn.textContent = 'COPY'; }, 1800); }); }); } } // ── 9. Global Keyboard Shortcuts ── function initKeyboardShortcuts() { window.addEventListener('keydown', (e) => { // Don't trigger when typing in inputs or textareas if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) { return; } if (e.key === '1') { switchTab('graph'); } else if (e.key === '2') { switchTab('staging'); } else if (e.key === '3') { switchTab('diff'); } else if (e.key === '4') { switchTab('branches'); } else if (e.key === 'h' || e.key === 'H') { jumpToHead(); showToast('Jumped to HEAD commit'); } else if (e.key === 'f' || e.key === 'F') { fitGraphToView(); showToast('Fitted DAG graph to viewport'); } else if (e.key === 'Escape') { const drawer = document.getElementById('commitInspector'); if (drawer) drawer.classList.remove('open'); } }); } function switchTab(tabId) { const tabBtn = document.querySelector(`.product-tab[data-tab="${tabId}"]`); if (tabBtn) tabBtn.click(); } // ── 10. Repository Link Notification ── function initRepoLinks() { document.querySelectorAll('.github-repo-link').forEach((link) => { link.addEventListener('click', (e) => { const href = link.getAttribute('href'); if (!href || href === '#' || href === '#downloads') { showToast('GitHub release repository link will be updated soon'); } }); }); } // ── Helpers ── function showToast(message) { const toast = document.getElementById('globalToast'); if (!toast) return; toast.textContent = message; toast.classList.add('show'); clearTimeout(toast._timeout); toast._timeout = setTimeout(() => { toast.classList.remove('show'); }, 2400); } function escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>'); } })();