diff --git a/frontend/css/git-playground.css b/frontend/css/git-playground.css index c6079c3..250d015 100644 --- a/frontend/css/git-playground.css +++ b/frontend/css/git-playground.css @@ -799,6 +799,7 @@ body.theme-matrix .window-controls .control { line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; + line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } @@ -831,4 +832,547 @@ body.theme-matrix .window-controls .control { .scenario-banner-actions button:hover { background: var(--primary-gold); color: #0b111a; +} + +/* ==================================================== + REPLAY MODE STYLES + ==================================================== */ + +/* --- Replay Toggle Button in Terminal Bar --- */ +.replay-toggle-btn { + background: transparent; + border: 1px solid transparent; + color: var(--text-secondary); + font-size: 1rem; + cursor: pointer; + padding: 0.3rem 0.5rem; + border-radius: 6px; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + gap: 4px; + position: relative; +} + +.replay-toggle-btn:hover { + color: var(--primary-gold); + border-color: rgba(212, 175, 55, 0.3); + background: rgba(212, 175, 55, 0.08); +} + +.replay-toggle-btn.active { + color: #ff6b6b; + border-color: rgba(255, 107, 107, 0.3); + background: rgba(255, 107, 107, 0.1); + animation: replayBtnPulse 2s ease-in-out infinite; +} + +@keyframes replayBtnPulse { + + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(255, 107, 107, 0); + } + + 50% { + box-shadow: 0 0 12px 2px rgba(255, 107, 107, 0.25); + } +} + +/* --- Replay Controls Bar --- */ +.replay-controls { + background: linear-gradient(135deg, rgba(22, 27, 34, 0.98), rgba(11, 17, 26, 0.98)); + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + backdrop-filter: blur(12px); + animation: slideDownReplay 0.35s ease-out; + flex-shrink: 0; +} + +@keyframes slideDownReplay { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.replay-controls-inner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1rem; + gap: 1rem; +} + +/* Replay "REC" label */ +.replay-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 1.5px; + text-transform: uppercase; + color: #ff6b6b; + white-space: nowrap; +} + +.replay-rec-dot { + font-size: 0.5rem; + animation: recBlink 1.2s ease-in-out infinite; +} + +@keyframes recBlink { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.2; + } +} + +/* Navigation buttons group */ +.replay-nav-btns { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.replay-btn { + background: rgba(48, 54, 61, 0.6); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 0.4rem 0.75rem; + border-radius: 8px; + cursor: pointer; + font-size: 0.8rem; + font-weight: 500; + font-family: 'Inter', sans-serif; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: all 0.25s ease; + white-space: nowrap; +} + +.replay-btn:hover:not(:disabled) { + background: rgba(212, 175, 55, 0.15); + border-color: var(--primary-gold); + color: var(--primary-gold); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(212, 175, 55, 0.15); +} + +.replay-btn:active:not(:disabled) { + transform: translateY(0); +} + +.replay-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.replay-btn i { + font-size: 0.75rem; +} + +/* Exit button special styling */ +.replay-exit-btn { + border-color: rgba(255, 107, 107, 0.3); + color: #ff6b6b; + background: rgba(255, 107, 107, 0.08); +} + +.replay-exit-btn:hover:not(:disabled) { + background: rgba(255, 107, 107, 0.2); + border-color: #ff6b6b; + color: #ff6b6b; + box-shadow: 0 4px 12px rgba(255, 107, 107, 0.15); +} + +/* Step indicator (e.g. 3 / 8) */ +.replay-step-indicator { + display: flex; + align-items: center; + gap: 0.3rem; + font-family: 'Fira Code', monospace; + font-size: 0.9rem; + padding: 0.3rem 0.8rem; + background: rgba(212, 175, 55, 0.08); + border: 1px solid rgba(212, 175, 55, 0.2); + border-radius: 8px; + min-width: 50px; + justify-content: center; +} + +.replay-step-current { + color: var(--primary-gold); + font-weight: 700; +} + +.replay-step-sep { + color: var(--text-secondary); +} + +.replay-step-total { + color: var(--text-secondary); + font-weight: 500; +} + +/* Progress Bar (below controls) */ +.replay-progress-track { + height: 3px; + background: rgba(48, 54, 61, 0.8); + overflow: hidden; +} + +.replay-progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-gold), #ffd700, var(--primary-gold)); + background-size: 200% 100%; + transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); + animation: progressShimmer 2s linear infinite; + border-radius: 0 2px 2px 0; +} + +@keyframes progressShimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +/* --- Replay History Timeline Panel --- */ +.replay-history-panel { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 280px; + background: linear-gradient(180deg, rgba(22, 27, 34, 0.97), rgba(11, 17, 26, 0.97)); + border-left: 1px solid var(--border-color); + backdrop-filter: blur(16px); + z-index: 10; + display: flex; + flex-direction: column; + animation: slideInRight 0.35s ease-out; + box-shadow: -8px 0 24px rgba(0, 0, 0, 0.3); +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +.replay-history-header { + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +.replay-history-header h4 { + margin: 0; + font-size: 0.85rem; + color: var(--primary-gold); + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.replay-history-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.replay-history-list::-webkit-scrollbar { + width: 4px; +} + +.replay-history-list::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 2px; +} + +/* Individual timeline step item */ +.replay-step-item { + display: flex; + align-items: flex-start; + gap: 0.6rem; + padding: 0.6rem 0.5rem; + border-radius: 8px; + cursor: pointer; + transition: all 0.25s ease; + position: relative; + margin-bottom: 2px; +} + +.replay-step-item:hover { + background: rgba(212, 175, 55, 0.06); +} + +.replay-step-item.active { + background: rgba(212, 175, 55, 0.1); + border: 1px solid rgba(212, 175, 55, 0.2); +} + +.replay-step-item.completed .replay-step-dot { + background: var(--primary-gold); + box-shadow: 0 0 8px rgba(212, 175, 55, 0.4); +} + +.replay-step-item.active .replay-step-dot { + background: var(--primary-gold); + box-shadow: 0 0 12px rgba(212, 175, 55, 0.6); + animation: dotPulse 1.5s ease-in-out infinite; +} + +@keyframes dotPulse { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.3); + } +} + +/* Timeline dot */ +.replay-step-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--border-color); + flex-shrink: 0; + margin-top: 4px; + transition: all 0.3s ease; +} + +/* Connecting line between dots */ +.replay-step-item:not(:last-child)::after { + content: ''; + position: absolute; + left: calc(0.5rem + 4px); + top: calc(0.6rem + 14px); + width: 2px; + height: calc(100% - 8px); + background: var(--border-color); +} + +.replay-step-item.completed:not(:last-child)::after { + background: rgba(212, 175, 55, 0.4); +} + +/* Step text info */ +.replay-step-info { + flex: 1; + min-width: 0; +} + +.replay-step-num { + font-size: 0.65rem; + color: var(--text-secondary); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 2px; +} + +.replay-step-cmd { + font-family: 'Fira Code', monospace; + font-size: 0.78rem; + color: var(--text-primary); + word-break: break-all; + line-height: 1.3; +} + +.replay-step-item.active .replay-step-cmd { + color: var(--primary-gold); +} + +.replay-step-item.future .replay-step-cmd { + color: var(--text-secondary); + opacity: 0.5; +} + +.replay-step-item.future .replay-step-dot { + opacity: 0.4; +} + +/* --- State Info Box (shown in terminal during replay) --- */ +.replay-state-box { + background: rgba(212, 175, 55, 0.06); + border: 1px solid rgba(212, 175, 55, 0.15); + border-radius: 8px; + padding: 0.8rem 1rem; + margin-top: 0.6rem; + font-size: 0.82rem; + line-height: 1.5; + animation: fadeInState 0.3s ease; +} + +@keyframes fadeInState { + from { + opacity: 0; + transform: translateY(4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.replay-state-box .state-label { + color: var(--primary-gold); + font-weight: 600; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 0.4rem; + display: flex; + align-items: center; + gap: 0.4rem; +} + +.replay-state-row { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.2rem; + color: var(--text-secondary); +} + +.replay-state-row .label { + color: #58a6ff; + font-weight: 500; + min-width: 70px; +} + +.replay-state-row .value { + color: var(--text-primary); + font-family: 'Fira Code', monospace; + font-size: 0.8rem; +} + +/* --- Read-Only Overlay --- */ +.replay-readonly-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(135deg, rgba(22, 27, 34, 0.95), rgba(11, 17, 26, 0.95)); + border-top: 1px solid rgba(255, 107, 107, 0.2); + padding: 0.6rem 1rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 0.8rem; + color: rgba(255, 107, 107, 0.7); + font-weight: 500; + letter-spacing: 0.5px; + backdrop-filter: blur(8px); + z-index: 20; +} + +.replay-readonly-overlay i { + font-size: 0.75rem; +} + +/* Terminal body needs relative positioning for the replay history panel */ +.terminal-window { + position: relative; +} + +/* Replay body state: reduce width when history panel is visible */ +.terminal-window.replay-active .terminal-body { + padding-right: 290px; + transition: padding-right 0.35s ease; +} + +/* --- Replay Mode: No-recording empty state --- */ +.replay-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 2rem; + text-align: center; + color: var(--text-secondary); +} + +.replay-empty-state i { + font-size: 2.5rem; + color: var(--border-color); + margin-bottom: 1rem; +} + +.replay-empty-state p { + font-size: 0.9rem; + margin: 0.3rem 0; + max-width: 250px; +} + +.replay-empty-state .hint { + font-size: 0.8rem; + color: var(--text-secondary); + opacity: 0.7; + margin-top: 0.5rem; +} + +/* --- Responsive --- */ +@media (max-width: 768px) { + .replay-controls-inner { + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem 0.8rem; + } + + .replay-nav-btns { + order: 2; + width: 100%; + justify-content: center; + } + + .replay-label { + order: 1; + } + + .replay-exit-btn { + order: 1; + } + + .replay-history-panel { + width: 220px; + } + + .terminal-window.replay-active .terminal-body { + padding-right: 230px; + } + + .replay-btn span { + display: none; + } } \ No newline at end of file diff --git a/frontend/js/git-playground.js b/frontend/js/git-playground.js index 59bd46e..828d142 100644 --- a/frontend/js/git-playground.js +++ b/frontend/js/git-playground.js @@ -17,6 +17,21 @@ document.addEventListener('DOMContentLoaded', () => { // Cheat Sheet UI const cheatSearch = document.getElementById('cheat-search'); + // Replay Mode UI + const replayToggleBtn = document.getElementById('replay-toggle-btn'); + const replayControls = document.getElementById('replay-controls'); + const replayPrevBtn = document.getElementById('replay-prev-btn'); + const replayNextBtn = document.getElementById('replay-next-btn'); + const replayRestartBtn = document.getElementById('replay-restart-btn'); + const replayExitBtn = document.getElementById('replay-exit-btn'); + const replayStepCurrent = document.getElementById('replay-step-current'); + const replayStepTotal = document.getElementById('replay-step-total'); + const replayProgressFill = document.getElementById('replay-progress-fill'); + const replayHistoryPanel = document.getElementById('replay-history-panel'); + const replayHistoryList = document.getElementById('replay-history-list'); + const replayReadonlyOverlay = document.getElementById('replay-readonly-overlay'); + const terminalInputArea = document.getElementById('terminal-input-area'); + // --- State --- const state = { history: [], @@ -32,6 +47,15 @@ document.addEventListener('DOMContentLoaded', () => { completedScenarios: [] }; + // --- Replay Mode State --- + const replayState = { + isActive: false, + snapshots: [], // Array of { cmd, terminalHTML, state: {...}, timestamp } + currentIndex: -1, // Current replay position (-1 = before any command) + savedTerminalHTML: '', // Terminal HTML before entering replay + savedState: null // Full state backup before entering replay + }; + // Load available scenarios from global or safe fallback const allScenarios = window.gitScenarios || []; @@ -377,6 +401,8 @@ document.addEventListener('DOMContentLoaded', () => { } function handleCommand() { + if (replayState.isActive) return; // Block commands during replay + const cmd = commandInput.value; if (!cmd.trim()) return; @@ -384,6 +410,233 @@ document.addEventListener('DOMContentLoaded', () => { addCommandToHistory(cmd); simulateGitCommand(cmd); commandInput.value = ''; + + // Record snapshot for replay AFTER command processes + setTimeout(() => { + recordReplaySnapshot(cmd); + }, state.commandDelay + 50); + } + + // ========================================================= + // REPLAY MODE SYSTEM + // ========================================================= + + /** + * Deep clone the current state for snapshot storage + */ + function cloneState() { + return { + currentDir: state.currentDir, + currentBranch: state.currentBranch, + branches: [...state.branches], + staged: [...state.staged], + commits: state.commits.map(c => ({ ...c })), + history: [...state.history] + }; + } + + /** + * Record a snapshot after each command execution + */ + function recordReplaySnapshot(cmd) { + replayState.snapshots.push({ + cmd: cmd, + terminalHTML: terminalOutput.innerHTML, + state: cloneState(), + timestamp: Date.now() + }); + } + + /** + * Enter Replay Mode + */ + function enterReplayMode() { + if (replayState.snapshots.length === 0) { + // No commands recorded yet — show empty state message + const currentHTML = terminalOutput.innerHTML; + terminalOutput.innerHTML = ` +
+ +

No commands recorded yet

+

Execute some Git commands first, then enter Replay Mode to review your session step-by-step.

+

Try: git init, git add ., git commit -m "first"

+
+ `; + setTimeout(() => { + terminalOutput.innerHTML = currentHTML; + }, 3000); + return; + } + + replayState.isActive = true; + replayState.savedTerminalHTML = terminalOutput.innerHTML; + replayState.savedState = cloneState(); + replayState.currentIndex = replayState.snapshots.length - 1; // Start at latest + + // UI Updates + replayToggleBtn.classList.add('active'); + replayControls.style.display = 'block'; + replayHistoryPanel.style.display = 'flex'; + replayReadonlyOverlay.style.display = 'flex'; + terminalInputArea.style.display = 'none'; + terminalWindow.classList.add('replay-active'); + + // Disable command input + commandInput.disabled = true; + sendBtn.disabled = true; + + renderReplayTimeline(); + goToReplayStep(replayState.currentIndex); + } + + /** + * Exit Replay Mode — restore original state + */ + function exitReplayMode() { + replayState.isActive = false; + + // Restore terminal + terminalOutput.innerHTML = replayState.savedTerminalHTML; + + // Restore state + if (replayState.savedState) { + state.currentDir = replayState.savedState.currentDir; + state.currentBranch = replayState.savedState.currentBranch; + state.branches = [...replayState.savedState.branches]; + state.staged = [...replayState.savedState.staged]; + state.commits = replayState.savedState.commits.map(c => ({ ...c })); + state.history = [...replayState.savedState.history]; + } + + // UI Updates + replayToggleBtn.classList.remove('active'); + replayControls.style.display = 'none'; + replayHistoryPanel.style.display = 'none'; + replayReadonlyOverlay.style.display = 'none'; + terminalInputArea.style.display = 'block'; + terminalWindow.classList.remove('replay-active'); + + // Re-enable command input + commandInput.disabled = false; + sendBtn.disabled = false; + commandInput.focus(); + } + + /** + * Navigate to a specific replay step + */ + function goToReplayStep(index) { + const total = replayState.snapshots.length; + if (index < 0) index = 0; + if (index >= total) index = total - 1; + + replayState.currentIndex = index; + const snapshot = replayState.snapshots[index]; + + // Update terminal output + terminalOutput.innerHTML = snapshot.terminalHTML; + + // Add state info box below the replayed output + const stateBox = document.createElement('div'); + stateBox.className = 'replay-state-box'; + const snap = snapshot.state; + stateBox.innerHTML = ` +
Repository State at Step ${index + 1}
+
+ Branch: + ${snap.currentBranch} +
+
+ Branches: + ${snap.branches.join(', ')} +
+
+ Staged: + ${snap.staged.length > 0 ? snap.staged.join(', ') : 'none'} +
+
+ Commits: + ${snap.commits.length} (latest: ${snap.commits[0]?.msg || 'none'}) +
+
+ Directory: + ${snap.currentDir} +
+ `; + terminalOutput.appendChild(stateBox); + terminalOutput.scrollTop = terminalOutput.scrollHeight; + + // Update controls + updateReplayControls(); + renderReplayTimeline(); + } + + /** + * Update the control buttons and indicators + */ + function updateReplayControls() { + const total = replayState.snapshots.length; + const idx = replayState.currentIndex; + + replayStepCurrent.textContent = idx + 1; + replayStepTotal.textContent = total; + + // Progress bar + const pct = total > 1 ? ((idx) / (total - 1)) * 100 : 100; + replayProgressFill.style.width = `${pct}%`; + + // Enable/disable buttons + replayPrevBtn.disabled = idx <= 0; + replayNextBtn.disabled = idx >= total - 1; + replayRestartBtn.disabled = idx <= 0; + } + + /** + * Render the timeline sidebar with all recorded steps + */ + function renderReplayTimeline() { + replayHistoryList.innerHTML = ''; + const currentIdx = replayState.currentIndex; + + replayState.snapshots.forEach((snap, i) => { + const item = document.createElement('div'); + item.className = 'replay-step-item'; + + // Assign visual states + if (i < currentIdx) item.classList.add('completed'); + else if (i === currentIdx) item.classList.add('active'); + else item.classList.add('future'); + + item.innerHTML = ` +
+
+
Step ${i + 1}
+
${escapeHtml(snap.cmd)}
+
+ `; + + // Click to jump to step + item.addEventListener('click', () => { + goToReplayStep(i); + }); + + replayHistoryList.appendChild(item); + }); + + // Scroll active step into view + const activeItem = replayHistoryList.querySelector('.replay-step-item.active'); + if (activeItem) { + activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + + /** + * Simple HTML escaper for safe display + */ + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } // --- Scenarios Management --- @@ -431,6 +684,12 @@ document.addEventListener('DOMContentLoaded', () => { } function loadScenario(id) { + if (replayState.isActive) exitReplayMode(); + + // Clear replay snapshots for new scenario session + replayState.snapshots = []; + replayState.currentIndex = -1; + const scenario = allScenarios.find(s => s.id === id); if (!scenario) return; @@ -487,11 +746,78 @@ document.addEventListener('DOMContentLoaded', () => { // Terminal Interactions commandInput.addEventListener('keydown', (e) => { + if (replayState.isActive) { e.preventDefault(); return; } if (e.key === 'Enter') handleCommand(); }); - sendBtn.addEventListener('click', handleCommand); - terminalOutput.addEventListener('click', () => commandInput.focus()); + sendBtn.addEventListener('click', () => { + if (replayState.isActive) return; + handleCommand(); + }); + terminalOutput.addEventListener('click', () => { + if (!replayState.isActive) commandInput.focus(); + }); + + // --- Replay Mode Event Listeners --- + if (replayToggleBtn) { + replayToggleBtn.addEventListener('click', () => { + if (replayState.isActive) { + exitReplayMode(); + } else { + enterReplayMode(); + } + }); + } + + if (replayPrevBtn) { + replayPrevBtn.addEventListener('click', () => { + if (replayState.currentIndex > 0) { + goToReplayStep(replayState.currentIndex - 1); + } + }); + } + + if (replayNextBtn) { + replayNextBtn.addEventListener('click', () => { + if (replayState.currentIndex < replayState.snapshots.length - 1) { + goToReplayStep(replayState.currentIndex + 1); + } + }); + } + + if (replayRestartBtn) { + replayRestartBtn.addEventListener('click', () => { + goToReplayStep(0); + }); + } + + if (replayExitBtn) { + replayExitBtn.addEventListener('click', () => { + exitReplayMode(); + }); + } + + // Keyboard shortcuts for replay navigation + document.addEventListener('keydown', (e) => { + if (!replayState.isActive) return; + + if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + e.preventDefault(); + if (replayState.currentIndex > 0) goToReplayStep(replayState.currentIndex - 1); + } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + e.preventDefault(); + if (replayState.currentIndex < replayState.snapshots.length - 1) goToReplayStep(replayState.currentIndex + 1); + } else if (e.key === 'Home') { + e.preventDefault(); + goToReplayStep(0); + } else if (e.key === 'End') { + e.preventDefault(); + goToReplayStep(replayState.snapshots.length - 1); + } else if (e.key === 'Escape') { + e.preventDefault(); + exitReplayMode(); + } + }); // Toggle View function switchView(view) { @@ -603,6 +929,13 @@ document.addEventListener('DOMContentLoaded', () => { if (clearHistoryBtn) { clearHistoryBtn.addEventListener('click', () => { + // Exit replay mode if active + if (replayState.isActive) exitReplayMode(); + + // Clear replay snapshots + replayState.snapshots = []; + replayState.currentIndex = -1; + state.history = []; state.currentDir = '~/projects'; state.currentBranch = 'main'; diff --git a/frontend/pages/git-playground.html b/frontend/pages/git-playground.html index 424a290..18e819b 100644 --- a/frontend/pages/git-playground.html +++ b/frontend/pages/git-playground.html @@ -13,7 +13,7 @@ rel="stylesheet"> - + @@ -41,6 +41,9 @@ 0 + @@ -58,7 +61,53 @@ -
+ + + + + + +
user@compass ~/projects @@ -67,6 +116,12 @@
+ + +
@@ -416,7 +471,7 @@

Achievement Badges

- +