Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git

Adds diffs

AuthorDave Jarvis <email>
Date2026-02-09 23:14:34 GMT-0800
Commit0568de7869627fc5ab575418ae3221b5cd499778
Parentd850750
DiffPage.php
+<?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>';
+ }
+}
GitDiff.php
+<?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;
+ }
+}
Router.php
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);
}
commit.png
Binary files differ
repo.css
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; }
Delta761 lines added, 294 lines removed, 467-line increase