From 309fb73c18b81c6acd602017d26f81a89cabe97c Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Sun, 5 Apr 2026 18:37:28 +0400 Subject: [PATCH 1/7] Handle 404 and other HTTP errors instead of showing "Loading..." forever Check response.ok on all fetch calls, show user-friendly error messages for 404 (not found), 403 (rate limited), and missing paths. Also adds basic CSS styling and file/folder icons to the listing. Co-Authored-By: Claude Opus 4.6 --- index.html | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index 4927dc3..f24ac5c 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,17 @@ raw-content +

.

@@ -20,7 +31,7 @@

.

const branch = path.shift(); if (!owner || !repo || !branch) { document.getElementById('dirname').textContent = 'Invalid URL format'; - document.getElementById('listing').textContent = `Expected: ${window.location.host}?OWNER/REPO/BRANCH/path/to/directory`; + document.getElementById('listing').innerHTML = '

Expected: ' + window.location.host + '?OWNER/REPO/BRANCH/path/to/directory

'; throw new Error('Invalid URL format'); } document.getElementById('dirname').textContent = window.location.search.substring(1); @@ -28,28 +39,40 @@

.

document.getElementsByTagName('title')[0].textContent = `${path.join('/') || '/'} | ${owner}/${repo}`; var listing = document.getElementById('listing'); +function showError(message) { + listing.innerHTML = '

' + message + '

'; +} + +function checkResponse(response) { + if (!response.ok) { + if (response.status === 404) throw new Error('Not found — check that the repository, branch, and path exist'); + if (response.status === 403) throw new Error('Rate limited — try again later or use an authenticated request'); + throw new Error(response.status + ' ' + response.statusText); + } + return response.json(); +} + function traverse(tree, path) { if (path.length === 0) return Promise.resolve(tree); const next = path.shift(); - return fetch(tree.tree.find(x => x.path === next).url) - .then(response => response.json()) - .then(subtree => traverse(subtree, path)) - .catch(error => { listing.textContent = 'Error loading directory: ' + error; }); + const entry = tree.tree.find(x => x.path === next); + if (!entry) throw new Error('Path not found: ' + next); + return fetch(entry.url).then(checkResponse).then(subtree => traverse(subtree, path)); } fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}`) - .then(response => response.json()) + .then(checkResponse) .then(tree => { traverse(tree, path).then(dir => { listing.innerHTML = ''; - }); + }).catch(error => showError(error.message)); }) - .catch(error => { listing.textContent = 'Error loading directory: ' + error; }); + .catch(error => showError(error.message)); From de1d10889c826df24abb35639d97c74a8fbcff31 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Sun, 5 Apr 2026 18:40:05 +0400 Subject: [PATCH 2/7] Redirect to raw file when path points to a blob, not a tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes "Cannot read properties of undefined (reading 'map')" when the URL targets a file like README.md — now redirects to the raw GitHub URL instead of trying to list it as a directory. Co-Authored-By: Claude Opus 4.6 --- index.html | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index f24ac5c..2a3d23b 100644 --- a/index.html +++ b/index.html @@ -53,24 +53,30 @@

.

} function traverse(tree, path) { - if (path.length === 0) return Promise.resolve(tree); + if (path.length === 0) return tree; const next = path.shift(); const entry = tree.tree.find(x => x.path === next); if (!entry) throw new Error('Path not found: ' + next); + if (entry.type === 'blob') { + var filePath = window.location.search.substring(1).split('/').slice(3).join('/'); + window.location.replace(`https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${filePath}`); + return null; + } return fetch(entry.url).then(checkResponse).then(subtree => traverse(subtree, path)); } +var subdir = window.location.search.substring(1).split('/').slice(3).join('/'); fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}`) .then(checkResponse) - .then(tree => { - traverse(tree, path).then(dir => { - listing.innerHTML = ''; - }).catch(error => showError(error.message)); + .then(tree => traverse(tree, path)) + .then(dir => { + if (!dir) return; + listing.innerHTML = ''; }) .catch(error => showError(error.message)); From a36162e98a4cb32f6e3f74c6f2cb220b1d02f55c Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Sun, 5 Apr 2026 18:40:47 +0400 Subject: [PATCH 3/7] Filter empty path segments from trailing slashes Splitting "owner/repo/main/" produces an empty string at the end, causing traverse to fail with "Path not found: ". Fix by filtering out empty segments after split. Co-Authored-By: Claude Opus 4.6 --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 2a3d23b..259c9a7 100644 --- a/index.html +++ b/index.html @@ -25,7 +25,7 @@

.

..
Loading...
From bc67a14700623da81c9168db32cd63716eab39de Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Sun, 5 Apr 2026 19:03:25 +0400 Subject: [PATCH 5/7] Separate repo header from path breadcrumb, add branch dropdown Split the breadcrumb into two distinct zones: - Repo header (owner / repo): immutable, links to GitHub, larger text with the repo name in bold - Toolbar: branch dropdown button + path breadcrumb on a subtle background, visually connected to the listing below The branch selector is a dropdown that lazy-loads branches from the GitHub API on first click, highlights the active branch with a checkmark, and navigates to the selected branch preserving the current path. Uses Octicon SVGs for the git-branch and chevron icons. Co-Authored-By: Claude Opus 4.6 --- index.html | 202 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 176 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index 36eec1f..ac8225d 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,7 @@ --color-bg-subtle: #f6f8fa; --color-border: #d1d9e0; --color-border-muted: #d8dee4; + --color-shadow: rgba(31, 35, 40, 0.12); --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; } @@ -31,6 +32,7 @@ --color-bg-subtle: #161b22; --color-border: #30363d; --color-border-muted: #21262d; + --color-shadow: rgba(0, 0, 0, 0.4); } } *, *::before, *::after { box-sizing: border-box; margin: 0; } @@ -47,19 +49,116 @@ a:hover { text-decoration: underline; } .container { max-width: 1012px; margin: 0 auto; } -/* --- Breadcrumb --- */ -.breadcrumb { - font-size: 16px; +/* --- Repo header (immutable) --- */ +.repo-header { + font-size: 20px; margin-bottom: 16px; line-height: 1.5; } -.breadcrumb .sep { color: var(--color-fg-muted); padding: 0 4px; } -.breadcrumb .cur { font-weight: 600; } +.repo-header a { color: var(--color-fg-accent); } +.repo-header .repo { font-weight: 600; } +.repo-header .sep { color: var(--color-fg-muted); padding: 0 4px; font-weight: 300; } + +/* --- Toolbar (branch + path) --- */ +.toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--color-bg-subtle); + border: 1px solid var(--color-border); + border-radius: 6px 6px 0 0; +} + +/* --- Branch selector --- */ +.branch-selector { position: relative; } +.branch-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 12px; + font-size: 14px; + font-family: var(--font); + color: var(--color-fg); + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 6px; + cursor: pointer; + white-space: nowrap; + line-height: 20px; +} +.branch-btn:hover { background: var(--color-bg-subtle); } +.branch-btn svg { fill: var(--color-fg-muted); flex-shrink: 0; } + +/* --- Branch dropdown --- */ +.branch-dropdown { + display: none; + position: absolute; + top: calc(100% + 4px); + left: 0; + min-width: 220px; + max-height: 300px; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 6px; + box-shadow: 0 8px 24px var(--color-shadow); + z-index: 100; +} +.branch-dropdown.open { display: flex; flex-direction: column; } +.dropdown-header { + padding: 8px 16px; + font-size: 12px; + font-weight: 600; + color: var(--color-fg-muted); + border-bottom: 1px solid var(--color-border-muted); + flex-shrink: 0; +} +.dropdown-list { + overflow-y: auto; + overscroll-behavior: contain; +} +.dropdown-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + color: var(--color-fg); + text-decoration: none; + font-size: 14px; + white-space: nowrap; + overflow: hidden; +} +.dropdown-item:hover { background: var(--color-bg-subtle); text-decoration: none; } +.dropdown-item.active { font-weight: 600; } +.dropdown-item .check { width: 16px; flex-shrink: 0; display: flex; } +.dropdown-item .check svg { fill: var(--color-fg); } +.dropdown-item .branch-name { + overflow: hidden; + text-overflow: ellipsis; +} +.dropdown-loading { + padding: 16px; + text-align: center; + color: var(--color-fg-muted); + font-size: 13px; +} + +/* --- Path breadcrumb --- */ +.path-crumb { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 2px; + min-width: 0; +} +.path-crumb .sep { color: var(--color-fg-muted); padding: 0 4px; } +.path-crumb .cur { font-weight: 600; } /* --- Listing --- */ .listing { border: 1px solid var(--color-border); - border-radius: 6px; + border-top: none; + border-radius: 0 0 6px 6px; overflow: hidden; } .row { @@ -125,7 +224,21 @@
- +
+
+
+ +
+ + +
+
+ +
@@ -133,6 +246,7 @@