| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-09 23:14:34 GMT-0800 |
| Commit | 0568de7869627fc5ab575418ae3221b5cd499778 |
| Parent | d850750 |
| +<?php | ||
| +require_once 'GitDiff.php'; | ||
| + | ||
| +class DiffPage extends BasePage { | ||
| + private $currentRepo; | ||
| + private $git; | ||
| + private $hash; | ||
| + | ||
| + public function __construct(array $repositories, array $currentRepo, Git $git, string $hash) { | ||
| + parent::__construct($repositories); | ||
| + $this->currentRepo = $currentRepo; | ||
| + $this->git = $git; | ||
| + $this->hash = $hash; | ||
| + $this->title = substr($hash, 0, 7); | ||
| + } | ||
| + | ||
| + public function render() { | ||
| + $this->renderLayout(function() { | ||
| + $commitData = $this->git->read($this->hash); | ||
| + $diffEngine = new GitDiff($this->git); | ||
| + | ||
| + $lines = explode("\n", $commitData); | ||
| + $msg = ''; | ||
| + $isMsg = false; | ||
| + $headers = []; | ||
| + | ||
| + foreach ($lines as $line) { | ||
| + if ($line === '') { $isMsg = true; continue; } | ||
| + if ($isMsg) { $msg .= $line . "\n"; } | ||
| + else { | ||
| + if (preg_match('/^(\w+) (.*)$/', $line, $m)) $headers[$m[1]] = $m[2]; | ||
| + } | ||
| + } | ||
| + | ||
| + $changes = $diffEngine->compare($this->hash); | ||
| + | ||
| + $this->renderBreadcrumbs(); | ||
| + | ||
| + echo '<div class="commit-details">'; | ||
| + echo '<div class="commit-header">'; | ||
| + echo '<h1 class="commit-title">' . htmlspecialchars(trim($msg)) . '</h1>'; | ||
| + echo '<div class="commit-info">'; | ||
| + echo '<div class="commit-info-row"><span class="commit-info-label">Author</span><span class="commit-author">' . htmlspecialchars($headers['author'] ?? 'Unknown') . '</span></div>'; | ||
| + echo '<div class="commit-info-row"><span class="commit-info-label">Commit</span><span class="commit-info-value">' . $this->hash . '</span></div>'; | ||
| + if (isset($headers['parent'])) { | ||
| + $repoUrl = '?repo=' . urlencode($this->currentRepo['safe_name']); | ||
| + echo '<div class="commit-info-row"><span class="commit-info-label">Parent</span><span class="commit-info-value">'; | ||
| + echo '<a href="?action=commit&hash=' . $headers['parent'] . $repoUrl . '" class="parent-link">' . substr($headers['parent'], 0, 7) . '</a>'; | ||
| + echo '</span></div>'; | ||
| + } | ||
| + echo '</div></div></div>'; | ||
| + | ||
| + echo '<div class="diff-container">'; | ||
| + | ||
| + foreach ($changes as $change) { | ||
| + $this->renderFileDiff($change); | ||
| + } | ||
| + | ||
| + if (empty($changes)) { | ||
| + echo '<div class="empty-state"><p>No changes detected.</p></div>'; | ||
| + } | ||
| + | ||
| + echo '</div>'; | ||
| + | ||
| + }, $this->currentRepo); | ||
| + } | ||
| + | ||
| + private function renderFileDiff($change) { | ||
| + $statusIcon = 'fa-file'; | ||
| + $statusClass = ''; | ||
| + | ||
| + if ($change['type'] === 'A') { $statusIcon = 'fa-plus-circle'; $statusClass = 'status-add'; } | ||
| + if ($change['type'] === 'D') { $statusIcon = 'fa-minus-circle'; $statusClass = 'status-del'; } | ||
| + if ($change['type'] === 'M') { $statusIcon = 'fa-pencil-alt'; $statusClass = 'status-mod'; } | ||
| + | ||
| + echo '<div class="diff-file">'; | ||
| + echo '<div class="diff-header">'; | ||
| + echo '<span class="diff-status ' . $statusClass . '"><i class="fa ' . $statusIcon . '"></i></span>'; | ||
| + echo '<span class="diff-path">' . htmlspecialchars($change['path']) . '</span>'; | ||
| + echo '</div>'; | ||
| + | ||
| + if ($change['is_binary']) { | ||
| + echo '<div class="diff-binary">Binary file not shown.</div>'; | ||
| + } else { | ||
| + echo '<div class="diff-content">'; | ||
| + echo '<table><tbody>'; | ||
| + | ||
| + $lineOld = 1; | ||
| + $lineNew = 1; | ||
| + | ||
| + foreach ($change['hunks'] as $line) { | ||
| + $class = 'diff-ctx'; | ||
| + $char = ' '; | ||
| + if ($line['t'] === '+') { $class = 'diff-add'; $char = '+'; } | ||
| + if ($line['t'] === '-') { $class = 'diff-del'; $char = '-'; } | ||
| + | ||
| + // Calculate line numbers | ||
| + $numOld = ($line['t'] !== '+') ? $lineOld++ : ''; | ||
| + $numNew = ($line['t'] !== '-') ? $lineNew++ : ''; | ||
| + | ||
| + echo '<tr class="' . $class . '">'; | ||
| + echo '<td class="diff-num" data-num="' . $numOld . '"></td>'; | ||
| + echo '<td class="diff-num" data-num="' . $numNew . '"></td>'; | ||
| + echo '<td class="diff-code"><span class="diff-marker">' . $char . '</span>' . htmlspecialchars($line['l']) . '</td>'; | ||
| + echo '</tr>'; | ||
| + } | ||
| + echo '</tbody></table>'; | ||
| + echo '</div>'; | ||
| + } | ||
| + echo '</div>'; | ||
| + } | ||
| + | ||
| + private function renderBreadcrumbs() { | ||
| + $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| + $crumbs = [ | ||
| + '<a href="?">Repositories</a>', | ||
| + '<a href="' . $repoUrl . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>', | ||
| + '<a href="?action=commits' . $repoUrl . '">Commits</a>', | ||
| + substr($this->hash, 0, 7) | ||
| + ]; | ||
| + echo '<div class="breadcrumb">' . implode(' / ', $crumbs) . '</div>'; | ||
| + } | ||
| +} | ||
| +<?php | ||
| +require_once 'File.php'; | ||
| + | ||
| +class GitDiff { | ||
| + private $git; | ||
| + | ||
| + public function __construct(Git $git) { | ||
| + $this->git = $git; | ||
| + } | ||
| + | ||
| + public function compare(string $commitHash) { | ||
| + // 1. Parse Commit to find Parent and Tree | ||
| + $commitData = $this->git->read($commitHash); | ||
| + $parentHash = ''; | ||
| + | ||
| + if (preg_match('/^parent ([0-9a-f]{40})/m', $commitData, $matches)) { | ||
| + $parentHash = $matches[1]; | ||
| + } | ||
| + | ||
| + $newTree = $this->getTreeHash($commitHash); | ||
| + $oldTree = $parentHash ? $this->getTreeHash($parentHash) : null; | ||
| + | ||
| + // 2. Diff the Trees | ||
| + return $this->diffTrees($oldTree, $newTree); | ||
| + } | ||
| + | ||
| + private function getTreeHash($commitSha) { | ||
| + $data = $this->git->read($commitSha); | ||
| + if (preg_match('/^tree ([0-9a-f]{40})/m', $data, $matches)) { | ||
| + return $matches[1]; | ||
| + } | ||
| + return null; | ||
| + } | ||
| + | ||
| + private function diffTrees($oldTreeSha, $newTreeSha, $path = '') { | ||
| + $changes = []; | ||
| + | ||
| + if ($oldTreeSha === $newTreeSha) return []; | ||
| + | ||
| + $oldEntries = $oldTreeSha ? $this->parseTree($oldTreeSha) : []; | ||
| + $newEntries = $newTreeSha ? $this->parseTree($newTreeSha) : []; | ||
| + | ||
| + $allNames = array_unique(array_merge(array_keys($oldEntries), array_keys($newEntries))); | ||
| + sort($allNames); | ||
| + | ||
| + foreach ($allNames as $name) { | ||
| + $old = $oldEntries[$name] ?? null; | ||
| + $new = $newEntries[$name] ?? null; | ||
| + $currentPath = $path ? "$path/$name" : $name; | ||
| + | ||
| + if (!$old) { | ||
| + // Added | ||
| + if ($new['is_dir']) { | ||
| + $changes = array_merge($changes, $this->diffTrees(null, $new['sha'], $currentPath)); | ||
| + } else { | ||
| + $changes[] = $this->createChange('A', $currentPath, null, $new['sha']); | ||
| + } | ||
| + } elseif (!$new) { | ||
| + // Deleted | ||
| + if ($old['is_dir']) { | ||
| + $changes = array_merge($changes, $this->diffTrees($old['sha'], null, $currentPath)); | ||
| + } else { | ||
| + $changes[] = $this->createChange('D', $currentPath, $old['sha'], null); | ||
| + } | ||
| + } elseif ($old['sha'] !== $new['sha']) { | ||
| + // Modified | ||
| + if ($old['is_dir'] && $new['is_dir']) { | ||
| + $changes = array_merge($changes, $this->diffTrees($old['sha'], $new['sha'], $currentPath)); | ||
| + } elseif (!$old['is_dir'] && !$new['is_dir']) { | ||
| + $changes[] = $this->createChange('M', $currentPath, $old['sha'], $new['sha']); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + return $changes; | ||
| + } | ||
| + | ||
| + private function parseTree($sha) { | ||
| + $data = $this->git->read($sha); | ||
| + $entries = []; | ||
| + $len = strlen($data); | ||
| + $pos = 0; | ||
| + | ||
| + while ($pos < $len) { | ||
| + $space = strpos($data, ' ', $pos); | ||
| + $null = strpos($data, "\0", $space); | ||
| + | ||
| + if ($space === false || $null === false) break; | ||
| + | ||
| + $mode = substr($data, $pos, $space - $pos); | ||
| + $name = substr($data, $space + 1, $null - $space - 1); | ||
| + $hash = bin2hex(substr($data, $null + 1, 20)); | ||
| + | ||
| + $entries[$name] = [ | ||
| + 'mode' => $mode, | ||
| + 'sha' => $hash, | ||
| + 'is_dir' => ($mode === '40000' || $mode === '040000') | ||
| + ]; | ||
| + | ||
| + $pos = $null + 21; | ||
| + } | ||
| + return $entries; | ||
| + } | ||
| + | ||
| + private function createChange($type, $path, $oldSha, $newSha) { | ||
| + $oldContent = $oldSha ? $this->git->read($oldSha) : ''; | ||
| + $newContent = $newSha ? $this->git->read($newSha) : ''; | ||
| + | ||
| + $isBinary = false; | ||
| + | ||
| + // Check New Content for Binary | ||
| + if ($newSha) { | ||
| + $f = new VirtualDiffFile($path, $newContent); | ||
| + if ($f->isBinary()) $isBinary = true; | ||
| + } | ||
| + // Check Old Content if New was fine or didn't exist | ||
| + if (!$isBinary && $oldSha) { | ||
| + $f = new VirtualDiffFile($path, $oldContent); | ||
| + if ($f->isBinary()) $isBinary = true; | ||
| + } | ||
| + | ||
| + $diff = null; | ||
| + if (!$isBinary) { | ||
| + $diff = $this->calculateDiff($oldContent, $newContent); | ||
| + } | ||
| + | ||
| + return [ | ||
| + 'type' => $type, | ||
| + 'path' => $path, | ||
| + 'is_binary' => $isBinary, | ||
| + 'hunks' => $diff | ||
| + ]; | ||
| + } | ||
| + | ||
| + private function calculateDiff($old, $new) { | ||
| + $oldLines = explode("\n", $old); | ||
| + $newLines = explode("\n", $new); | ||
| + | ||
| + // Simple LCS (Longest Common Subsequence) implementation | ||
| + $m = count($oldLines); | ||
| + $n = count($newLines); | ||
| + | ||
| + // Skip identical lines at start | ||
| + $start = 0; | ||
| + while ($start < $m && $start < $n && $oldLines[$start] === $newLines[$start]) { | ||
| + $start++; | ||
| + } | ||
| + | ||
| + // Skip identical lines at end | ||
| + $end = 0; | ||
| + while ($m - $end > $start && $n - $end > $start && $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end]) { | ||
| + $end++; | ||
| + } | ||
| + | ||
| + $oldSlice = array_slice($oldLines, $start, $m - $start - $end); | ||
| + $newSlice = array_slice($newLines, $start, $n - $start - $end); | ||
| + | ||
| + $ops = $this->computeLCS($oldSlice, $newSlice); | ||
| + | ||
| + $finalDiff = []; | ||
| + | ||
| + // Add Context (Top) | ||
| + for ($i = 0; $i < $start; $i++) { | ||
| + $finalDiff[] = ['t' => ' ', 'l' => $oldLines[$i]]; | ||
| + } | ||
| + | ||
| + // Add Changes | ||
| + foreach ($ops as $op) { | ||
| + $finalDiff[] = $op; | ||
| + } | ||
| + | ||
| + // Add Context (Bottom) | ||
| + for ($i = $m - $end; $i < $m; $i++) { | ||
| + $finalDiff[] = ['t' => ' ', 'l' => $oldLines[$i]]; | ||
| + } | ||
| + | ||
| + return $finalDiff; | ||
| + } | ||
| + | ||
| + private function computeLCS($old, $new) { | ||
| + $m = count($old); | ||
| + $n = count($new); | ||
| + $c = array_fill(0, $m + 1, array_fill(0, $n + 1, 0)); | ||
| + | ||
| + for ($i = 1; $i <= $m; $i++) { | ||
| + for ($j = 1; $j <= $n; $j++) { | ||
| + if ($old[$i-1] === $new[$j-1]) { | ||
| + $c[$i][$j] = $c[$i-1][$j-1] + 1; | ||
| + } else { | ||
| + $c[$i][$j] = max($c[$i][$j-1], $c[$i-1][$j]); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + $diff = []; | ||
| + $i = $m; $j = $n; | ||
| + while ($i > 0 || $j > 0) { | ||
| + if ($i > 0 && $j > 0 && $old[$i-1] === $new[$j-1]) { | ||
| + array_unshift($diff, ['t' => ' ', 'l' => $old[$i-1]]); | ||
| + $i--; $j--; | ||
| + } elseif ($j > 0 && ($i === 0 || $c[$i][$j-1] >= $c[$i-1][$j])) { | ||
| + array_unshift($diff, ['t' => '+', 'l' => $new[$j-1]]); | ||
| + $j--; | ||
| + } elseif ($i > 0 && ($j === 0 || $c[$i][$j-1] < $c[$i-1][$j])) { | ||
| + array_unshift($diff, ['t' => '-', 'l' => $old[$i-1]]); | ||
| + $i--; | ||
| + } | ||
| + } | ||
| + return $diff; | ||
| + } | ||
| +} | ||
| + | ||
| +class VirtualDiffFile extends File { | ||
| + private $content; | ||
| + | ||
| + public function __construct($name, $content) { | ||
| + parent::__construct($name, '', '100644', 0, strlen($content)); | ||
| + $this->content = $content; | ||
| + } | ||
| + | ||
| + public function isBinary(): bool { | ||
| + $buffer = substr($this->content, 0, 12); | ||
| + return MediaTypeSniffer::isBinary($buffer, $this->name); | ||
| + } | ||
| + | ||
| + public function __get($prop) { | ||
| + if ($prop === 'name') return $this->name; | ||
| + return null; | ||
| + } | ||
| +} | ||
| require_once 'RepositoryList.php'; | ||
| require_once 'Git.php'; | ||
| +require_once 'GitDiff.php'; | ||
| +require_once 'DiffPage.php'; | ||
| class Router { | ||
| if ($action === 'raw') { | ||
| return new RawPage($this->git, $hash); | ||
| + } | ||
| + | ||
| + if ($action === 'commit') { | ||
| + return new DiffPage($this->repositories, $currentRepo, $this->git, $hash); | ||
| } | ||
| padding: 12px 16px; | ||
| margin-bottom: 20px; | ||
| - color: #8b949e; /* Color for the / separators */ | ||
| -} | ||
| - | ||
| -.breadcrumb a { | ||
| - color: #58a6ff; | ||
| - text-decoration: none; | ||
| -} | ||
| - | ||
| -.breadcrumb a:hover { | ||
| - text-decoration: underline; | ||
| -} | ||
| - | ||
| -.blob-content { | ||
| - background: #161b22; | ||
| - border: 1px solid #30363d; | ||
| - border-radius: 6px; | ||
| - overflow: hidden; | ||
| -} | ||
| - | ||
| -.blob-header { | ||
| - background: #21262d; | ||
| - padding: 12px 16px; | ||
| - border-bottom: 1px solid #30363d; | ||
| - font-size: 0.875rem; | ||
| - color: #8b949e; | ||
| -} | ||
| - | ||
| -.blob-code { | ||
| - padding: 16px; | ||
| - overflow-x: auto; | ||
| - font-family: 'SFMono-Regular', Consolas, monospace; | ||
| - font-size: 0.875rem; | ||
| - line-height: 1.6; | ||
| - white-space: pre; | ||
| -} | ||
| - | ||
| -.refs-list { | ||
| - display: grid; | ||
| - gap: 10px; | ||
| -} | ||
| - | ||
| -.ref-item { | ||
| - background: #161b22; | ||
| - border: 1px solid #30363d; | ||
| - border-radius: 6px; | ||
| - padding: 12px 16px; | ||
| - display: flex; | ||
| - align-items: center; | ||
| - gap: 12px; | ||
| -} | ||
| - | ||
| -.ref-type { | ||
| - background: #238636; | ||
| - color: white; | ||
| - padding: 2px 8px; | ||
| - border-radius: 12px; | ||
| - font-size: 0.75rem; | ||
| - font-weight: 600; | ||
| - text-transform: uppercase; | ||
| -} | ||
| - | ||
| -.ref-type.tag { | ||
| - background: #8957e5; | ||
| -} | ||
| - | ||
| -.ref-name { | ||
| - font-weight: 600; | ||
| - color: #f0f6fc; | ||
| -} | ||
| - | ||
| -.empty-state { | ||
| - text-align: center; | ||
| - padding: 60px 20px; | ||
| - color: #8b949e; | ||
| -} | ||
| - | ||
| -.commit-details { | ||
| - background: #161b22; | ||
| - border: 1px solid #30363d; | ||
| - border-radius: 6px; | ||
| - padding: 20px; | ||
| - margin-bottom: 20px; | ||
| -} | ||
| - | ||
| -.commit-header { | ||
| - margin-bottom: 20px; | ||
| -} | ||
| - | ||
| -.commit-title { | ||
| - font-size: 1.25rem; | ||
| - color: #f0f6fc; | ||
| - margin-bottom: 10px; | ||
| -} | ||
| - | ||
| -.commit-info { | ||
| - display: grid; | ||
| - gap: 8px; | ||
| - font-size: 0.875rem; | ||
| -} | ||
| - | ||
| -.commit-info-row { | ||
| - display: flex; | ||
| - gap: 10px; | ||
| -} | ||
| - | ||
| -.commit-info-label { | ||
| - color: #8b949e; | ||
| - width: 80px; | ||
| - flex-shrink: 0; | ||
| -} | ||
| - | ||
| -.commit-info-value { | ||
| - color: #c9d1d9; | ||
| - font-family: monospace; | ||
| -} | ||
| - | ||
| -.parent-link { | ||
| - color: #58a6ff; | ||
| - text-decoration: none; | ||
| -} | ||
| - | ||
| -.parent-link:hover { | ||
| - text-decoration: underline; | ||
| -} | ||
| - | ||
| -.repo-grid { | ||
| - display: grid; | ||
| - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| - gap: 16px; | ||
| - margin-top: 20px; | ||
| -} | ||
| - | ||
| -.repo-card { | ||
| - background: #161b22; | ||
| - border: 1px solid #30363d; | ||
| - border-radius: 8px; | ||
| - padding: 20px; | ||
| - text-decoration: none; | ||
| - color: inherit; | ||
| - transition: border-color 0.2s, transform 0.1s; | ||
| -} | ||
| - | ||
| -.repo-card:hover { | ||
| - border-color: #58a6ff; | ||
| - transform: translateY(-2px); | ||
| -} | ||
| - | ||
| -.repo-card h3 { | ||
| - color: #58a6ff; | ||
| - margin-bottom: 8px; | ||
| - font-size: 1.1rem; | ||
| -} | ||
| - | ||
| -.repo-card p { | ||
| - color: #8b949e; | ||
| - font-size: 0.875rem; | ||
| - margin: 0; | ||
| -} | ||
| - | ||
| -.current-repo { | ||
| - background: #21262d; | ||
| - border: 1px solid #58a6ff; | ||
| - padding: 8px 16px; | ||
| - border-radius: 6px; | ||
| - font-size: 0.875rem; | ||
| - color: #f0f6fc; | ||
| -} | ||
| - | ||
| -.current-repo strong { | ||
| - color: #58a6ff; | ||
| -} | ||
| - | ||
| -.branch-badge { | ||
| - background: #238636; | ||
| - color: white; | ||
| - padding: 2px 8px; | ||
| - border-radius: 12px; | ||
| - font-size: 0.75rem; | ||
| - font-weight: 600; | ||
| - margin-left: 10px; | ||
| -} | ||
| - | ||
| -.commit-row { | ||
| - display: flex; | ||
| - padding: 10px 0; | ||
| - border-bottom: 1px solid #30363d; | ||
| - gap: 15px; | ||
| - align-items: baseline; | ||
| -} | ||
| - | ||
| -.commit-row:last-child { | ||
| - border-bottom: none; | ||
| -} | ||
| - | ||
| -.commit-row .sha { | ||
| - font-family: monospace; | ||
| - color: #58a6ff; | ||
| - text-decoration: none; | ||
| -} | ||
| - | ||
| -.commit-row .message { | ||
| - flex: 1; | ||
| - font-weight: 500; | ||
| -} | ||
| - | ||
| -.commit-row .meta { | ||
| - font-size: 0.85em; | ||
| - color: #8b949e; | ||
| - white-space: nowrap; | ||
| -} | ||
| - | ||
| -.blob-content-image { | ||
| - text-align: center; | ||
| - padding: 20px; | ||
| - background: #0d1117; | ||
| -} | ||
| - | ||
| -.blob-content-image img { | ||
| - max-width: 100%; | ||
| - border: 1px solid #30363d; | ||
| -} | ||
| - | ||
| -.blob-content-video { | ||
| - text-align: center; | ||
| - padding: 20px; | ||
| - background: #000; | ||
| -} | ||
| - | ||
| -.blob-content-video video { | ||
| - max-width: 100%; | ||
| - max-height: 80vh; | ||
| -} | ||
| - | ||
| -.blob-content-audio { | ||
| - text-align: center; | ||
| - padding: 40px; | ||
| - background: #161b22; | ||
| -} | ||
| - | ||
| -.blob-content-audio audio { | ||
| - width: 100%; | ||
| - max-width: 600px; | ||
| -} | ||
| - | ||
| -.download-state { | ||
| - text-align: center; | ||
| - padding: 40px; | ||
| - border: 1px solid #30363d; | ||
| - border-radius: 6px; | ||
| - margin-top: 10px; | ||
| -} | ||
| - | ||
| -.download-state p { | ||
| - margin-bottom: 20px; | ||
| - color: #8b949e; | ||
| -} | ||
| - | ||
| -.btn-download { | ||
| - display: inline-block; | ||
| - padding: 6px 16px; | ||
| - background: #238636; | ||
| - color: white; | ||
| - text-decoration: none; | ||
| - border-radius: 6px; | ||
| - font-weight: 600; | ||
| -} | ||
| - | ||
| -.repo-info-banner { | ||
| - margin-top: 15px; | ||
| -} | ||
| - | ||
| -.file-icon-container { | ||
| - width: 20px; | ||
| - text-align: center; | ||
| - margin-right: 5px; | ||
| - color: #8b949e; | ||
| -} | ||
| - | ||
| -.file-size { | ||
| - color: #8b949e; | ||
| - font-size: 0.8em; | ||
| - margin-left: 10px; | ||
| -} | ||
| - | ||
| -.file-date { | ||
| - color: #8b949e; | ||
| - font-size: 0.8em; | ||
| - margin-left: auto; | ||
| -} | ||
| - | ||
| -.repo-card-time { | ||
| - margin-top: 8px; | ||
| - color: #58a6ff; | ||
| -} | ||
| + color: #8b949e; | ||
| +} | ||
| + | ||
| +.breadcrumb a { | ||
| + color: #58a6ff; | ||
| + text-decoration: none; | ||
| +} | ||
| + | ||
| +.breadcrumb a:hover { | ||
| + text-decoration: underline; | ||
| +} | ||
| + | ||
| +.blob-content { | ||
| + background: #161b22; | ||
| + border: 1px solid #30363d; | ||
| + border-radius: 6px; | ||
| + overflow: hidden; | ||
| +} | ||
| + | ||
| +.blob-header { | ||
| + background: #21262d; | ||
| + padding: 12px 16px; | ||
| + border-bottom: 1px solid #30363d; | ||
| + font-size: 0.875rem; | ||
| + color: #8b949e; | ||
| +} | ||
| + | ||
| +.blob-code { | ||
| + padding: 16px; | ||
| + overflow-x: auto; | ||
| + font-family: 'SFMono-Regular', Consolas, monospace; | ||
| + font-size: 0.875rem; | ||
| + line-height: 1.6; | ||
| + white-space: pre; | ||
| +} | ||
| + | ||
| +.refs-list { | ||
| + display: grid; | ||
| + gap: 10px; | ||
| +} | ||
| + | ||
| +.ref-item { | ||
| + background: #161b22; | ||
| + border: 1px solid #30363d; | ||
| + border-radius: 6px; | ||
| + padding: 12px 16px; | ||
| + display: flex; | ||
| + align-items: center; | ||
| + gap: 12px; | ||
| +} | ||
| + | ||
| +.ref-type { | ||
| + background: #238636; | ||
| + color: white; | ||
| + padding: 2px 8px; | ||
| + border-radius: 12px; | ||
| + font-size: 0.75rem; | ||
| + font-weight: 600; | ||
| + text-transform: uppercase; | ||
| +} | ||
| + | ||
| +.ref-type.tag { | ||
| + background: #8957e5; | ||
| +} | ||
| + | ||
| +.ref-name { | ||
| + font-weight: 600; | ||
| + color: #f0f6fc; | ||
| +} | ||
| + | ||
| +.empty-state { | ||
| + text-align: center; | ||
| + padding: 60px 20px; | ||
| + color: #8b949e; | ||
| +} | ||
| + | ||
| +.commit-details { | ||
| + background: #161b22; | ||
| + border: 1px solid #30363d; | ||
| + border-radius: 6px; | ||
| + padding: 20px; | ||
| + margin-bottom: 20px; | ||
| +} | ||
| + | ||
| +.commit-header { | ||
| + margin-bottom: 20px; | ||
| +} | ||
| + | ||
| +.commit-title { | ||
| + font-size: 1.25rem; | ||
| + color: #f0f6fc; | ||
| + margin-bottom: 10px; | ||
| +} | ||
| + | ||
| +.commit-info { | ||
| + display: grid; | ||
| + gap: 8px; | ||
| + font-size: 0.875rem; | ||
| +} | ||
| + | ||
| +.commit-info-row { | ||
| + display: flex; | ||
| + gap: 10px; | ||
| +} | ||
| + | ||
| +.commit-info-label { | ||
| + color: #8b949e; | ||
| + width: 80px; | ||
| + flex-shrink: 0; | ||
| +} | ||
| + | ||
| +.commit-info-value { | ||
| + color: #c9d1d9; | ||
| + font-family: monospace; | ||
| +} | ||
| + | ||
| +.parent-link { | ||
| + color: #58a6ff; | ||
| + text-decoration: none; | ||
| +} | ||
| + | ||
| +.parent-link:hover { | ||
| + text-decoration: underline; | ||
| +} | ||
| + | ||
| +.repo-grid { | ||
| + display: grid; | ||
| + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| + gap: 16px; | ||
| + margin-top: 20px; | ||
| +} | ||
| + | ||
| +.repo-card { | ||
| + background: #161b22; | ||
| + border: 1px solid #30363d; | ||
| + border-radius: 8px; | ||
| + padding: 20px; | ||
| + text-decoration: none; | ||
| + color: inherit; | ||
| + transition: border-color 0.2s, transform 0.1s; | ||
| +} | ||
| + | ||
| +.repo-card:hover { | ||
| + border-color: #58a6ff; | ||
| + transform: translateY(-2px); | ||
| +} | ||
| + | ||
| +.repo-card h3 { | ||
| + color: #58a6ff; | ||
| + margin-bottom: 8px; | ||
| + font-size: 1.1rem; | ||
| +} | ||
| + | ||
| +.repo-card p { | ||
| + color: #8b949e; | ||
| + font-size: 0.875rem; | ||
| + margin: 0; | ||
| +} | ||
| + | ||
| +.current-repo { | ||
| + background: #21262d; | ||
| + border: 1px solid #58a6ff; | ||
| + padding: 8px 16px; | ||
| + border-radius: 6px; | ||
| + font-size: 0.875rem; | ||
| + color: #f0f6fc; | ||
| +} | ||
| + | ||
| +.current-repo strong { | ||
| + color: #58a6ff; | ||
| +} | ||
| + | ||
| +.branch-badge { | ||
| + background: #238636; | ||
| + color: white; | ||
| + padding: 2px 8px; | ||
| + border-radius: 12px; | ||
| + font-size: 0.75rem; | ||
| + font-weight: 600; | ||
| + margin-left: 10px; | ||
| +} | ||
| + | ||
| +.commit-row { | ||
| + display: flex; | ||
| + padding: 10px 0; | ||
| + border-bottom: 1px solid #30363d; | ||
| + gap: 15px; | ||
| + align-items: baseline; | ||
| +} | ||
| + | ||
| +.commit-row:last-child { | ||
| + border-bottom: none; | ||
| +} | ||
| + | ||
| +.commit-row .sha { | ||
| + font-family: monospace; | ||
| + color: #58a6ff; | ||
| + text-decoration: none; | ||
| +} | ||
| + | ||
| +.commit-row .message { | ||
| + flex: 1; | ||
| + font-weight: 500; | ||
| +} | ||
| + | ||
| +.commit-row .meta { | ||
| + font-size: 0.85em; | ||
| + color: #8b949e; | ||
| + white-space: nowrap; | ||
| +} | ||
| + | ||
| +.blob-content-image { | ||
| + text-align: center; | ||
| + padding: 20px; | ||
| + background: #0d1117; | ||
| +} | ||
| + | ||
| +.blob-content-image img { | ||
| + max-width: 100%; | ||
| + border: 1px solid #30363d; | ||
| +} | ||
| + | ||
| +.blob-content-video { | ||
| + text-align: center; | ||
| + padding: 20px; | ||
| + background: #000; | ||
| +} | ||
| + | ||
| +.blob-content-video video { | ||
| + max-width: 100%; | ||
| + max-height: 80vh; | ||
| +} | ||
| + | ||
| +.blob-content-audio { | ||
| + text-align: center; | ||
| + padding: 40px; | ||
| + background: #161b22; | ||
| +} | ||
| + | ||
| +.blob-content-audio audio { | ||
| + width: 100%; | ||
| + max-width: 600px; | ||
| +} | ||
| + | ||
| +.download-state { | ||
| + text-align: center; | ||
| + padding: 40px; | ||
| + border: 1px solid #30363d; | ||
| + border-radius: 6px; | ||
| + margin-top: 10px; | ||
| +} | ||
| + | ||
| +.download-state p { | ||
| + margin-bottom: 20px; | ||
| + color: #8b949e; | ||
| +} | ||
| + | ||
| +.btn-download { | ||
| + display: inline-block; | ||
| + padding: 6px 16px; | ||
| + background: #238636; | ||
| + color: white; | ||
| + text-decoration: none; | ||
| + border-radius: 6px; | ||
| + font-weight: 600; | ||
| +} | ||
| + | ||
| +.repo-info-banner { | ||
| + margin-top: 15px; | ||
| +} | ||
| + | ||
| +.file-icon-container { | ||
| + width: 20px; | ||
| + text-align: center; | ||
| + margin-right: 5px; | ||
| + color: #8b949e; | ||
| +} | ||
| + | ||
| +.file-size { | ||
| + color: #8b949e; | ||
| + font-size: 0.8em; | ||
| + margin-left: 10px; | ||
| +} | ||
| + | ||
| +.file-date { | ||
| + color: #8b949e; | ||
| + font-size: 0.8em; | ||
| + margin-left: auto; | ||
| +} | ||
| + | ||
| +.repo-card-time { | ||
| + margin-top: 8px; | ||
| + color: #58a6ff; | ||
| +} | ||
| + | ||
| + | ||
| +/* --- GIT DIFF STYLES (Protanopia Dark) --- */ | ||
| + | ||
| +.diff-container { | ||
| + display: flex; | ||
| + flex-direction: column; | ||
| + gap: 20px; | ||
| +} | ||
| + | ||
| +.diff-file { | ||
| + background: #161b22; | ||
| + border: 1px solid #30363d; | ||
| + border-radius: 6px; | ||
| + overflow: hidden; | ||
| +} | ||
| + | ||
| +.diff-header { | ||
| + background: #21262d; | ||
| + padding: 10px 16px; | ||
| + border-bottom: 1px solid #30363d; | ||
| + display: flex; | ||
| + align-items: center; | ||
| + gap: 10px; | ||
| +} | ||
| + | ||
| +.diff-path { | ||
| + font-family: monospace; | ||
| + font-size: 0.9rem; | ||
| + color: #f0f6fc; | ||
| +} | ||
| + | ||
| +.diff-binary { | ||
| + padding: 20px; | ||
| + text-align: center; | ||
| + color: #8b949e; | ||
| + font-style: italic; | ||
| +} | ||
| + | ||
| +.diff-content { | ||
| + overflow-x: auto; | ||
| +} | ||
| + | ||
| +.diff-content table { | ||
| + width: 100%; | ||
| + border-collapse: collapse; | ||
| + font-family: 'SFMono-Regular', Consolas, monospace; | ||
| + font-size: 12px; | ||
| +} | ||
| + | ||
| +.diff-content td { | ||
| + padding: 2px 0; | ||
| + line-height: 20px; | ||
| +} | ||
| + | ||
| +.diff-num { | ||
| + width: 1%; | ||
| + min-width: 40px; | ||
| + text-align: right; | ||
| + padding-right: 10px; | ||
| + color: #6e7681; | ||
| + user-select: none; | ||
| + background: #0d1117; | ||
| + border-right: 1px solid #30363d; | ||
| +} | ||
| + | ||
| +.diff-num::before { | ||
| + content: attr(data-num); | ||
| +} | ||
| + | ||
| +.diff-code { | ||
| + padding-left: 10px; | ||
| + white-space: pre-wrap; | ||
| + word-break: break-all; | ||
| + color: #c9d1d9; | ||
| +} | ||
| + | ||
| +.diff-marker { | ||
| + display: inline-block; | ||
| + width: 15px; | ||
| + user-select: none; | ||
| + color: #8b949e; | ||
| +} | ||
| + | ||
| +/* Protanopia Safe Colors: Blue (Add) and Yellow (Del) */ | ||
| +.diff-add { | ||
| + background-color: rgba(2, 59, 149, 0.25); | ||
| +} | ||
| +.diff-add .diff-code { | ||
| + color: #79c0ff; | ||
| +} | ||
| +.diff-add .diff-marker { | ||
| + color: #79c0ff; | ||
| +} | ||
| + | ||
| +.diff-del { | ||
| + background-color: rgba(148, 99, 0, 0.25); | ||
| +} | ||
| +.diff-del .diff-code { | ||
| + color: #d29922; | ||
| +} | ||
| +.diff-del .diff-marker { | ||
| + color: #d29922; | ||
| +} | ||
| + | ||
| +.status-add { color: #58a6ff; } | ||
| +.status-del { color: #d29922; } | ||
| +.status-mod { color: #a371f7; } | ||
| Delta | 761 lines added, 294 lines removed, 467-line increase |
|---|