Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 })();