gh-here 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/gh-here.js CHANGED
@@ -9,7 +9,8 @@ const { setupRoutes } = require('../lib/server');
9
9
 
10
10
  // Parse command line arguments
11
11
  const args = process.argv.slice(2);
12
- const openBrowser = args.includes('--open') || args.includes('-o');
12
+ const noOpen = args.includes('--no-open');
13
+ const openBrowser = !noOpen; // Default is to open browser
13
14
  const helpRequested = args.includes('--help') || args.includes('-h');
14
15
 
15
16
  // Check for port specification
@@ -37,17 +38,17 @@ gh-here - GitHub-like local file browser
37
38
  Usage: npx gh-here [options]
38
39
 
39
40
  Options:
40
- --open, -o Open browser automatically
41
+ --no-open Do not open browser automatically
41
42
  --browser=<name> Specify browser (safari, chrome, firefox, arc)
42
43
  --port=<number> Specify port number (default: 5555)
43
44
  --help, -h Show this help message
44
45
 
45
46
  Examples:
46
- npx gh-here Start server on port 5555 (or next available)
47
- npx gh-here --open Start server and open browser
48
- npx gh-here --port=8080 Start server on port 8080
49
- npx gh-here --open --browser=safari Start server and open in Safari
50
- npx gh-here --open --browser=arc Start server and open in Arc
47
+ npx gh-here Start server and open browser
48
+ npx gh-here --no-open Start server without opening browser
49
+ npx gh-here --port=8080 Start server on port 8080 and open browser
50
+ npx gh-here --browser=safari Start server and open in Safari
51
+ npx gh-here --browser=arc Start server and open in Arc
51
52
  `);
52
53
  process.exit(0);
53
54
  }
@@ -181,8 +182,6 @@ async function startServer() {
181
182
  if (openBrowser) {
182
183
  console.log(`🌍 Opening browser...`);
183
184
  setTimeout(() => openBrowserToUrl(url), 1000);
184
- } else {
185
- console.log(`💡 Tip: Use --open flag to launch browser automatically`);
186
185
  }
187
186
  });
188
187
  } catch (error) {
package/blog-post.md ADDED
@@ -0,0 +1,100 @@
1
+ # Why gh-here is the Perfect Tool for Terminal-Heavy Developers
2
+
3
+ If you're like most developers in 2025, you probably spend a lot of time in the terminal. You're using Claude Code to generate and refactor code, running commands left and right, and iterating quickly on projects that haven't even been pushed to GitHub yet. But sometimes you need to step back and get a bird's-eye view of your codebase—and that's where `gh-here` shines.
4
+
5
+ ## The Gap Between Terminal and IDE
6
+
7
+ Picture this scenario: You're deep in a terminal session, using Claude Code to implement a new feature. You've got files scattered across multiple directories, you're testing things with curl, and you're in that flow state where opening a heavy IDE would just break your momentum. But you need to:
8
+
9
+ - Quickly browse through your project structure
10
+ - Check what files you've modified
11
+ - Preview a README or markdown file
12
+ - Navigate between related files without losing context
13
+ - See language distribution across your codebase
14
+
15
+ This is the exact gap that `gh-here` fills.
16
+
17
+ ## Built for Modern Development Workflows
18
+
19
+ ### 1. **Perfect for AI-Assisted Development**
20
+ When you're working with Claude Code or GitHub Copilot, you're often generating and modifying files rapidly. `gh-here` gives you an instant, GitHub-like interface to review what's been changed without committing to version control or opening a full IDE.
21
+
22
+ ### 2. **Ideal for Pre-Push Workflows**
23
+ Not everything needs to hit GitHub immediately. Whether you're experimenting with a proof of concept or working on a feature branch, `gh-here` lets you navigate and review your local changes in a familiar, web-based interface.
24
+
25
+ ### 3. **Terminal-Native but GUI-Friendly**
26
+ Launch it with a single command (`gh-here`) from any directory, and instantly get a clean, GitHub-inspired interface in your browser. No configuration, no setup—just point and browse.
27
+
28
+ ## Real-World Use Cases
29
+
30
+ **Scenario 1: Code Review Before Push**
31
+ ```bash
32
+ $ claude "implement user authentication system"
33
+ # ... lots of generated files and changes
34
+ $ gh-here
35
+ # Browse through generated files, check git status, review changes
36
+ $ git add . && git commit -m "Add user authentication"
37
+ ```
38
+
39
+ **Scenario 2: Project Exploration**
40
+ ```bash
41
+ $ git clone some-interesting-repo
42
+ $ cd some-interesting-repo
43
+ $ gh-here
44
+ # Instantly get an overview: languages used, file structure, README preview
45
+ ```
46
+
47
+ **Scenario 3: Documentation Review**
48
+ ```bash
49
+ $ claude "update all the documentation"
50
+ $ gh-here
51
+ # Preview all the markdown files in GitHub-style rendering
52
+ # Check if everything looks good before committing
53
+ ```
54
+
55
+ ## Why Not Just Use GitHub or an IDE?
56
+
57
+ **GitHub**: Your code isn't pushed yet, or you're working on experimental changes you don't want in version control.
58
+
59
+ **IDE**: Too heavy for quick browsing, breaks your terminal flow, takes time to open and index.
60
+
61
+ **File Explorer**: No syntax highlighting, no git integration, no markdown rendering, no language stats.
62
+
63
+ **Terminal**: Great for editing, terrible for browsing and getting visual context.
64
+
65
+ ## The Sweet Spot
66
+
67
+ `gh-here` hits the perfect sweet spot:
68
+ - **Lightweight**: Starts instantly, no heavy indexing
69
+ - **Familiar**: GitHub-style interface that every developer knows
70
+ - **Integrated**: Shows git status, handles various file types, renders markdown
71
+ - **Flexible**: Works with any directory, whether it's a git repo or not
72
+ - **Terminal-friendly**: Launch with one command, works alongside your existing workflow
73
+
74
+ ## Features That Just Make Sense
75
+
76
+ - **Language statistics**: Instantly see what technologies your project uses
77
+ - **Git integration**: Visual git status indicators, diff viewing
78
+ - **File type support**: Syntax highlighting, image viewing, markdown rendering
79
+ - **Search functionality**: Quick file search with keyboard shortcuts
80
+ - **Breadcrumb navigation**: Always know where you are in your project
81
+
82
+ ## Perfect for 2025 Development
83
+
84
+ As development workflows become more AI-assisted and terminal-centric, tools like `gh-here` become essential. You're not always in an IDE, you're not always ready to push to GitHub, but you always need to understand your codebase structure and changes.
85
+
86
+ It's the missing link between your terminal workflow and the visual context you need to stay productive.
87
+
88
+ ## Get Started
89
+
90
+ ```bash
91
+ npm install -g gh-here
92
+ cd your-project
93
+ gh-here
94
+ ```
95
+
96
+ That's it. Your local codebase is now browsable in a clean, GitHub-style interface at `localhost:5556`.
97
+
98
+ ---
99
+
100
+ *Built for developers who live in the terminal but occasionally need a better view of their code.*
package/lib/file-utils.js CHANGED
@@ -256,9 +256,53 @@ function formatBytes(bytes) {
256
256
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
257
257
  }
258
258
 
259
+ /**
260
+ * File type detection functions
261
+ */
262
+ function isImageFile(filePathOrExt) {
263
+ const ext = getExtension(filePathOrExt);
264
+ const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico'];
265
+ return imageExtensions.includes(ext);
266
+ }
267
+
268
+ function isBinaryFile(filePathOrExt) {
269
+ const ext = getExtension(filePathOrExt);
270
+ const binaryExtensions = [
271
+ // Images (handled separately)
272
+ 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico',
273
+ // Archives
274
+ 'zip', 'tar', 'gz', 'rar', '7z',
275
+ // Executables
276
+ 'exe', 'bin', 'app', 'deb', 'rpm',
277
+ // Documents
278
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
279
+ // Media
280
+ 'mp4', 'mov', 'avi', 'mkv', 'mp3', 'wav', 'flac',
281
+ // Other
282
+ 'class', 'so', 'dll', 'dylib'
283
+ ];
284
+ return binaryExtensions.includes(ext);
285
+ }
286
+
287
+ function isTextFile(filePathOrExt) {
288
+ return !isBinaryFile(filePathOrExt);
289
+ }
290
+
291
+ function getExtension(filePathOrExt) {
292
+ // If it's already just an extension (no dots), return as-is
293
+ if (!filePathOrExt.includes('.') && !filePathOrExt.includes('/') && !filePathOrExt.includes('\\')) {
294
+ return filePathOrExt.toLowerCase();
295
+ }
296
+ // Extract extension from file path
297
+ return path.extname(filePathOrExt).toLowerCase().slice(1);
298
+ }
299
+
259
300
  module.exports = {
260
301
  getFileIcon,
261
302
  getLanguageFromExtension,
262
303
  getLanguageColor,
263
- formatBytes
304
+ formatBytes,
305
+ isImageFile,
306
+ isBinaryFile,
307
+ isTextFile
264
308
  };
package/lib/renderers.js CHANGED
@@ -5,7 +5,7 @@ const marked = require('marked');
5
5
  const octicons = require('@primer/octicons');
6
6
  const { exec } = require('child_process');
7
7
 
8
- const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes } = require('./file-utils');
8
+ const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes, isImageFile, isBinaryFile, isTextFile } = require('./file-utils');
9
9
  const { getGitStatusIcon, getGitStatusDescription } = require('./git');
10
10
 
11
11
  /**
@@ -18,7 +18,8 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
18
18
  const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch, workingDirName);
19
19
  const readmeFile = findReadmeFile(items);
20
20
  const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
21
- const languageStats = generateLanguageStats(items);
21
+ // Only show language stats on root/top-level directory
22
+ const languageStats = (!currentPath || currentPath === '.') ? generateLanguageStats(items) : '';
22
23
 
23
24
  const itemsHtml = items.map(item => `
24
25
  <tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
@@ -113,6 +114,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
113
114
  ${octicons['chevron-down'].toSVG({ class: 'octicon-chevron' })}
114
115
  </button>
115
116
  ` : ''}
117
+ ${languageStats}
116
118
  </div>
117
119
  <div class="repo-controls-right">
118
120
  <div class="search-container">
@@ -130,7 +132,6 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
130
132
  <main>
131
133
  <div class="repo-canvas">
132
134
  <div class="repo-canvas-content">
133
- ${languageStats}
134
135
  <div class="file-table-container">
135
136
  <table class="file-table" id="file-table">
136
137
  <thead>
@@ -242,8 +243,9 @@ function generateLanguageStats(items) {
242
243
  `;
243
244
  }
244
245
 
245
- async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot) {
246
- const breadcrumbs = generateBreadcrumbs(filePath);
246
+ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir = null) {
247
+ const workingDirName = workingDir ? path.basename(workingDir) : null;
248
+ const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
247
249
 
248
250
  // Get git diff for the file
249
251
  return new Promise((resolve, reject) => {
@@ -369,8 +371,10 @@ function renderRawDiff(diffOutput, ext) {
369
371
  return `<div class="raw-diff-container">${linesHtml}</div>`;
370
372
  }
371
373
 
372
- async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir) {
373
- const breadcrumbs = generateBreadcrumbs(filePath);
374
+
375
+ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir = null) {
376
+ const workingDirName = workingDir ? path.basename(workingDir) : null;
377
+ const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
374
378
  let displayContent;
375
379
  let viewToggle = '';
376
380
 
@@ -378,7 +382,49 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
378
382
  const absolutePath = path.resolve(path.join(workingDir, filePath));
379
383
  const hasGitChanges = gitStatus && gitStatus[absolutePath];
380
384
 
381
- if (ext === 'md') {
385
+ // Determine file category and handle accordingly
386
+ if (isImageFile(ext)) {
387
+ // Handle image files
388
+ const imageUrl = `/download?path=${encodeURIComponent(filePath)}`;
389
+ displayContent = `
390
+ <div class="image-container">
391
+ <img src="${imageUrl}" alt="${path.basename(filePath)}" class="image-display">
392
+ <div class="image-info">
393
+ <span class="image-filename">${path.basename(filePath)}</span>
394
+ </div>
395
+ </div>
396
+ `;
397
+
398
+ // Add diff view for images with git changes
399
+ if (hasGitChanges) {
400
+ const currentParams = new URLSearchParams({ path: filePath });
401
+ const diffUrl = `/?${currentParams.toString()}&view=diff`;
402
+
403
+ viewToggle = `
404
+ <div class="view-toggle">
405
+ <a href="/?path=${encodeURIComponent(filePath)}" class="view-btn active">
406
+ ${octicons.eye.toSVG({ class: 'view-icon' })} View
407
+ </a>
408
+ <a href="${diffUrl}" class="view-btn">
409
+ ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
410
+ </a>
411
+ </div>
412
+ `;
413
+ }
414
+ } else if (isBinaryFile(ext) && !isImageFile(ext)) {
415
+ // Handle other binary files - download only
416
+ displayContent = `
417
+ <div class="binary-file-container">
418
+ <div class="binary-file-info">
419
+ <h3>Binary File</h3>
420
+ <p>This is a binary file that cannot be displayed in the browser.</p>
421
+ <a href="/download?path=${encodeURIComponent(filePath)}" class="btn btn-primary" download="${path.basename(filePath)}">
422
+ ${octicons.download.toSVG({ class: 'download-icon' })} Download File
423
+ </a>
424
+ </div>
425
+ </div>
426
+ `;
427
+ } else if (ext === 'md') {
382
428
  if (viewMode === 'raw') {
383
429
  const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
384
430
 
@@ -468,9 +514,11 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
468
514
  <div id="filename-input-container" class="filename-input-container" style="display: none;">
469
515
  <input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
470
516
  </div>
471
- <button id="edit-btn" class="edit-btn" aria-label="Edit file">
472
- ${octicons.pencil.toSVG({ class: 'edit-icon' })}
473
- </button>
517
+ ${isTextFile(ext) ? `
518
+ <button id="edit-btn" class="edit-btn" aria-label="Edit file">
519
+ ${octicons.pencil.toSVG({ class: 'edit-icon' })}
520
+ </button>
521
+ ` : ''}
474
522
  ${viewToggle}
475
523
  <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
476
524
  ${octicons.moon.toSVG({ class: 'theme-icon' })}
@@ -484,6 +532,7 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
484
532
  <div class="file-content">
485
533
  ${displayContent}
486
534
  </div>
535
+ ${isTextFile(ext) ? `
487
536
  <div id="editor-container" class="editor-container" style="display: none;">
488
537
  <div class="editor-header">
489
538
  <div class="editor-title">Edit ${path.basename(filePath)}</div>
@@ -497,6 +546,7 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
497
546
  <div id="file-editor" class="monaco-editor"></div>
498
547
  </div>
499
548
  </div>
549
+ ` : ''}
500
550
  </div>
501
551
  </main>
502
552
  </body>
@@ -504,8 +554,9 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
504
554
  `;
505
555
  }
506
556
 
507
- function renderNewFile(currentPath) {
508
- const breadcrumbs = generateBreadcrumbs(currentPath);
557
+ function renderNewFile(currentPath, workingDir = null) {
558
+ const workingDirName = workingDir ? path.basename(workingDir) : null;
559
+ const breadcrumbs = generateBreadcrumbs(currentPath, null, workingDirName);
509
560
 
510
561
  return `
511
562
  <!DOCTYPE html>
package/lib/server.js CHANGED
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const { getGitStatus, getGitBranch, commitAllChanges, commitSelectedFiles, getGitDiff } = require('./git');
6
6
  const { isIgnoredByGitignore, getGitignoreRules } = require('./gitignore');
7
7
  const { renderDirectory, renderFileDiff, renderFile, renderNewFile } = require('./renderers');
8
+ const { isImageFile, isBinaryFile } = require('./file-utils');
8
9
 
9
10
  /**
10
11
  * Express server setup and route handlers
@@ -16,6 +17,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
16
17
  app.use('/static', express.static(path.join(__dirname, '..', 'public')));
17
18
  app.use('/octicons', express.static(path.join(__dirname, '..', 'node_modules', '@primer', 'octicons', 'build')));
18
19
 
20
+
19
21
  // Download route
20
22
  app.get('/download', (req, res) => {
21
23
  const filePath = req.query.path || '';
@@ -25,8 +27,16 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
25
27
  const stats = fs.statSync(fullPath);
26
28
  if (stats.isFile()) {
27
29
  const fileName = path.basename(fullPath);
28
- res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
29
- res.sendFile(fullPath);
30
+
31
+ // For images, serve inline for viewing, otherwise force download
32
+ if (isImageFile(fullPath)) {
33
+ // Serve image inline
34
+ res.sendFile(fullPath);
35
+ } else {
36
+ // Force download for non-images
37
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
38
+ res.sendFile(fullPath);
39
+ }
30
40
  } else {
31
41
  res.status(400).send('Cannot download directories');
32
42
  }
@@ -84,16 +94,21 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
84
94
 
85
95
  res.send(renderDirectory(currentPath, items, showGitignored, gitBranch, gitStatus, workingDir));
86
96
  } else {
87
- const content = fs.readFileSync(fullPath, 'utf8');
88
97
  const ext = path.extname(fullPath).slice(1);
89
98
  const viewMode = req.query.view || 'rendered';
90
99
 
100
+ // For image files, don't try to read as text
101
+ let content = '';
102
+ if (!isImageFile(fullPath)) {
103
+ content = fs.readFileSync(fullPath, 'utf8');
104
+ }
105
+
91
106
  if (viewMode === 'diff' && isGitRepo) {
92
107
  // Check if file has git status
93
108
  const absolutePath = path.resolve(fullPath);
94
109
  const gitInfo = gitStatus[absolutePath];
95
110
  if (gitInfo) {
96
- const diffHtml = await renderFileDiff(currentPath, ext, gitInfo, gitRepoRoot);
111
+ const diffHtml = await renderFileDiff(currentPath, ext, gitInfo, gitRepoRoot, workingDir);
97
112
  return res.send(diffHtml);
98
113
  }
99
114
  }
@@ -108,7 +123,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
108
123
  // Route for creating new files
109
124
  app.get('/new', (req, res) => {
110
125
  const currentPath = req.query.path || '';
111
- res.send(renderNewFile(currentPath));
126
+ res.send(renderNewFile(currentPath, workingDir));
112
127
  });
113
128
 
114
129
  // API endpoint to get file content for editing
@@ -122,6 +137,11 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
122
137
  return res.status(403).send('Access denied');
123
138
  }
124
139
 
140
+ // Check if it's a binary file - prevent editing
141
+ if (isBinaryFile(fullPath)) {
142
+ return res.status(400).json({ error: 'Cannot edit binary files' });
143
+ }
144
+
125
145
  const content = fs.readFileSync(fullPath, 'utf-8');
126
146
  res.send(content);
127
147
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-here",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "A local GitHub-like file browser for viewing code",
5
5
  "repository": "https://github.com/corywilkerson/gh-here",
6
6
  "main": "index.js",
package/public/styles.css CHANGED
@@ -499,29 +499,54 @@ main {
499
499
 
500
500
  /* Old search styles removed - using new GitHub-style search */
501
501
 
502
- /* Language stats */
502
+ /* Language stats in control bar */
503
503
  .language-stats {
504
504
  display: flex;
505
- gap: 16px;
506
- margin-bottom: 16px;
507
- padding: 12px 0;
508
- border-bottom: 1px solid var(--border-primary);
509
- flex-wrap: wrap;
505
+ align-items: center;
506
+ gap: 8px;
507
+ margin-left: 16px;
508
+ max-width: 300px;
509
+ overflow: hidden;
510
+ position: relative;
511
+ }
512
+
513
+ .language-stats:hover {
514
+ overflow: visible;
515
+ z-index: 10;
516
+ }
517
+
518
+ .language-stats:hover::before {
519
+ content: '';
520
+ position: absolute;
521
+ top: -4px;
522
+ left: -8px;
523
+ right: -8px;
524
+ bottom: -4px;
525
+ background: var(--bg-secondary);
526
+ border: 1px solid var(--border-primary);
527
+ border-radius: 6px;
528
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
529
+ z-index: -1;
530
+ pointer-events: none;
510
531
  }
511
532
 
512
533
  .lang-stat {
513
534
  display: flex;
514
535
  align-items: center;
515
- gap: 6px;
516
- font-size: 12px;
536
+ gap: 4px;
537
+ font-size: 11px;
517
538
  color: var(--text-secondary);
539
+ white-space: nowrap;
540
+ flex-shrink: 0;
541
+ padding: 2px 0;
518
542
  }
519
543
 
520
544
  .lang-dot {
521
- width: 12px;
522
- height: 12px;
545
+ width: 8px;
546
+ height: 8px;
523
547
  border-radius: 50%;
524
548
  display: inline-block;
549
+ flex-shrink: 0;
525
550
  }
526
551
 
527
552
  .lang-name {
@@ -722,6 +747,83 @@ main {
722
747
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
723
748
  }
724
749
 
750
+ /* Image viewing styles */
751
+ .image-container {
752
+ padding: 20px;
753
+ text-align: center;
754
+ background-color: var(--bg-primary);
755
+ border-radius: 6px;
756
+ }
757
+
758
+ .image-display {
759
+ max-width: 100%;
760
+ max-height: 80vh;
761
+ height: auto;
762
+ border-radius: 6px;
763
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
764
+ background: var(--bg-secondary);
765
+ }
766
+
767
+ [data-theme="light"] .image-display {
768
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
769
+ }
770
+
771
+ .image-info {
772
+ margin-top: 16px;
773
+ padding-top: 16px;
774
+ border-top: 1px solid var(--border-primary);
775
+ }
776
+
777
+ .image-filename {
778
+ font-size: 14px;
779
+ font-weight: 600;
780
+ color: var(--text-primary);
781
+ }
782
+
783
+ /* Binary file viewing styles */
784
+ .binary-file-container {
785
+ padding: 40px 20px;
786
+ text-align: center;
787
+ background-color: var(--bg-primary);
788
+ border-radius: 6px;
789
+ border: 1px solid var(--border-primary);
790
+ }
791
+
792
+ .binary-file-info h3 {
793
+ font-size: 18px;
794
+ font-weight: 600;
795
+ color: var(--text-primary);
796
+ margin-bottom: 12px;
797
+ }
798
+
799
+ .binary-file-info p {
800
+ font-size: 14px;
801
+ color: var(--text-secondary);
802
+ margin-bottom: 20px;
803
+ }
804
+
805
+ .binary-file-info .btn {
806
+ display: inline-flex;
807
+ align-items: center;
808
+ gap: 8px;
809
+ padding: 8px 16px;
810
+ border-radius: 6px;
811
+ font-size: 14px;
812
+ font-weight: 500;
813
+ text-decoration: none;
814
+ border: 1px solid transparent;
815
+ cursor: pointer;
816
+ }
817
+
818
+ .binary-file-info .btn-primary {
819
+ background-color: var(--link-color);
820
+ color: white;
821
+ }
822
+
823
+ .binary-file-info .btn-primary:hover {
824
+ opacity: 0.9;
825
+ }
826
+
725
827
  /* Line numbers for code viewing */
726
828
  .with-line-numbers {
727
829
  counter-reset: line;
@@ -1933,6 +2035,31 @@ main {
1933
2035
  height: 14px;
1934
2036
  }
1935
2037
 
2038
+ /* General modal overlay */
2039
+ .modal-overlay {
2040
+ position: fixed;
2041
+ top: 0;
2042
+ left: 0;
2043
+ right: 0;
2044
+ bottom: 0;
2045
+ background: rgba(0, 0, 0, 0.7);
2046
+ display: flex;
2047
+ align-items: center;
2048
+ justify-content: center;
2049
+ z-index: 1000;
2050
+ }
2051
+
2052
+ .modal-content {
2053
+ background: var(--bg-primary);
2054
+ border: 1px solid var(--border-primary);
2055
+ border-radius: 8px;
2056
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
2057
+ padding: 20px;
2058
+ max-width: 90vw;
2059
+ max-height: 90vh;
2060
+ overflow-y: auto;
2061
+ }
2062
+
1936
2063
  /* Clean commit modal */
1937
2064
  .commit-modal-overlay {
1938
2065
  position: fixed;