vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Turn the code tab into an editor-style file browser
a464f8927bfb3edcf170694f120dc9b3f68320da · Keenan Nemetz · 15d ago · parent f09ef39e
Turn the code tab into an editor-style file browser - Add a persistent nested file tree with HTMX file and folder swaps, active rows, expanded ancestors, and deep-link support. - Share one page header across every repository tab: title and summary on the left, tab actions grouped on the right. - Make the repository name the first tab and show visibility with a lock or globe icon derived from the ACL. - Open markdown as syntax-highlighted source by default, with an explicit full-width preview toggle. - Point the branch breadcrumb at the Code overview instead of a second root directory listing. - Move contributor-branch CI approval from the code browser to the Runs tab. - Extend UI tests for the file tree, fragments, repo visibility, markdown source/preview, shared headers, and the moved CI control.
server/web/static/app.js +32
| @@ -144,3 +144,35 @@ | |||
| 144 | 144 | // HTMX replaces a sidebar section with a fresh token input; wire it up again. | |
| 145 | 145 | document.body.addEventListener('htmx:afterSwap', (event) => init(event.target)); | |
| 146 | 146 | })(); | |
| 147 | |||
| 148 | // Code browser: keep the file tree's active row and expanded ancestors in sync | ||
| 149 | // when HTMX replaces the main pane. The full-page render already resolves this | ||
| 150 | // server-side; this only closes the gap for fragment navigation. | ||
| 151 | (() => { | ||
| 152 | const sync = (main) => { | ||
| 153 | const tree = document.getElementById('file-tree'); | ||
| 154 | if (!tree || !main) return; | ||
| 155 | const path = main.dataset.path || ''; | ||
| 156 | tree.querySelectorAll('.active').forEach((el) => el.classList.remove('active')); | ||
| 157 | if (!path) return; | ||
| 158 | const node = Array.from(tree.querySelectorAll('[data-path]')).find((el) => el.dataset.path === path); | ||
| 159 | if (!node) return; | ||
| 160 | node.classList.add('active'); | ||
| 161 | let details = node.matches('details') ? node : node.closest('details'); | ||
| 162 | while (details) { | ||
| 163 | details.open = true; | ||
| 164 | details = details.parentElement.closest('details'); | ||
| 165 | } | ||
| 166 | if (!node.matches('details')) node.scrollIntoView({ block: 'nearest' }); | ||
| 167 | }; | ||
| 168 | document.body.addEventListener('htmx:afterSwap', (event) => { | ||
| 169 | const main = event.target; | ||
| 170 | if (!main || main.id !== 'code-main') return; | ||
| 171 | sync(main); | ||
| 172 | // If the old file had scrolled the page down, the next file should start at | ||
| 173 | // its own top rather than midway through. | ||
| 174 | if (main.getBoundingClientRect().top < 60) { | ||
| 175 | main.scrollIntoView({ block: 'start' }); | ||
| 176 | } | ||
| 177 | }); | ||
| 178 | })(); | ||