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/vole.css +156−20

@@ -149,15 +149,6 @@ img { max-width: 100%; }
149 149 border-bottom: 1px solid var(--border);
150 150 }
151 151
152 .reponame {
153 color: var(--fg);
154 font-size: 16px;
155 font-weight: 650;
156 letter-spacing: -0.01em;
157 padding: 0 2px 10px;
158 }
159 .reponame:hover { text-decoration: none; color: var(--accent); }
160
161 152 .tabs {
162 153 display: flex;
163 154 gap: 2px;
@@ -175,6 +166,7 @@ img { max-width: 100%; }
175 166 }
176 167 .tabs a:hover { color: var(--fg); text-decoration: none; }
177 168 .tabs a.active { color: var(--fg); border-bottom-color: var(--accent); }
169 .tabs a.repotab { color: var(--fg); font-weight: 600; }
178 170
179 171 /* ---------- panels and tables ---------- */
180 172
@@ -343,9 +335,6 @@ a.label:hover { color: var(--accent); border-color: var(--accent); text-decorati
343 335
344 336 /* ---------- repo home ---------- */
345 337
346 .repo-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin: 0 0 14px; }
347 .repo-sub .item { color: var(--fg-muted); font-size: 12.5px; }
348
349 338 details.clone { position: relative; }
350 339 details.clone > summary {
351 340 list-style: none;
@@ -503,7 +492,6 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; }
503 492 @media (max-width: 720px) {
504 493 h1 { font-size: 16px; }
505 494 .repohead { gap: 0 10px; }
506 .reponame { font-size: 15px; }
507 495 th, td { padding: 7px 9px; }
508 496 .nowrap { white-space: normal; }
509 497 }
@@ -583,6 +571,7 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; }
583 571 font-family: var(--mono);
584 572 font-size: 12px;
585 573 }
574 .filehead .icon { width: 13px; height: 13px; margin-right: 6px; vertical-align: -2px; }
586 575 .filehead a { font-family: var(--font); font-size: 11.5px; color: var(--fg-muted); }
587 576 .filehead a:hover { color: var(--accent); }
588 577 .readme {
@@ -658,15 +647,150 @@ td.fit { width: 1%; white-space: nowrap; }
658 647
659 648 /* ---------- code browsing ---------- */
660 649
661 .path-title {
650 /* The tab bar and the tree header share a height so their content starts on the
651 same line. The bar is the file's identity, not a separate title block. */
652 .code-tabbar {
653 display: flex;
654 align-items: center;
655 gap: 8px;
656 height: 36px;
657 min-width: 0;
658 padding: 0 10px;
659 background: var(--bg-raised);
660 border: 1px solid var(--border);
661 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
662 color: var(--fg-muted);
662 663 font-family: var(--mono);
663 font-size: 16px;
664 font-weight: 550;
665 word-break: break-all;
666 margin-bottom: 4px;
664 font-size: 11.5px;
665 }
666 .code-tab-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); }
667 .code-tab-icon .icon { width: 13px; height: 13px; }
668 .code-crumbs {
669 display: flex;
670 align-items: center;
671 gap: 6px;
672 min-width: 0;
673 overflow: hidden;
674 white-space: nowrap;
675 }
676 .code-crumbs a { color: var(--fg-muted); }
677 .code-crumbs a:hover { color: var(--accent); }
678 .code-crumbs .sep { color: var(--fg-faint); }
679 .code-crumbs .current { color: var(--fg); overflow: hidden; text-overflow: ellipsis; }
680 .code-tab-actions {
681 display: flex;
682 flex: 0 0 auto;
683 align-items: center;
684 gap: 10px;
685 margin-left: auto;
686 font-family: var(--font);
687 font-size: 11.5px;
688 }
689 .code-tab-actions a { color: var(--fg-muted); }
690 .code-tab-actions a:hover,
691 .code-tab-actions a.active { color: var(--accent); }
692 .code-tab-actions .muted { font-size: 11.5px; }
693 .code-tabbar + .panel,
694 .code-tabbar + .highlight,
695 .code-tabbar + .filebody,
696 .code-tabbar + .card,
697 .code-tabbar + .readme {
698 margin-top: 0;
699 border-top: 0;
700 border-top-left-radius: 0;
701 border-top-right-radius: 0;
702 }
703
704 /* ---------- code browser ---------- */
705
706 .code-browser {
707 display: grid;
708 grid-template-columns: 260px minmax(0, 1fr);
709 gap: 18px;
710 align-items: start;
711 }
712 .code-main { min-width: 0; scroll-margin-top: 60px; }
713
714 .file-tree {
715 display: flex;
716 flex-direction: column;
717 position: sticky;
718 top: 66px;
719 max-height: calc(100vh - 86px);
720 overflow: hidden;
721 background: var(--bg-panel);
722 border: 1px solid var(--border);
723 border-radius: var(--radius-lg);
724 }
725 .tree-head {
726 display: flex;
727 align-items: center;
728 justify-content: space-between;
729 gap: 8px;
730 height: 36px;
731 padding: 0 10px;
732 background: var(--bg-raised);
733 border-bottom: 1px solid var(--border);
734 color: var(--fg-muted);
735 font-size: 11px;
736 letter-spacing: 0.05em;
737 text-transform: uppercase;
738 }
739 .tree-head .icon { width: 13px; height: 13px; margin-right: 5px; vertical-align: -2px; }
740 .tree-head .muted { font-size: 10.5px; letter-spacing: 0; text-transform: none; }
741 .tree-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 6px 4px 8px; }
742
743 .tree-list { list-style: none; margin: 0; padding: 0; }
744 .tree-children { margin-left: 9px; padding-left: 5px; border-left: 1px solid var(--border); }
745 .tree-row {
746 display: flex;
747 align-items: center;
748 gap: 6px;
749 min-height: 24px;
750 padding: 2px 7px;
751 border-radius: 4px;
752 color: var(--fg-muted);
753 font-size: 12px;
754 line-height: 1.45;
755 }
756 summary.tree-row { list-style: none; cursor: pointer; }
757 summary.tree-row::-webkit-details-marker { display: none; }
758 .tree-row:hover { background: var(--bg-hover); color: var(--fg); text-decoration: none; }
759 .tree-row.active,
760 .tree-dir.active > summary.tree-row { background: var(--accent-soft); color: var(--accent); }
761 .tree-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); }
762 .tree-row.active .tree-icon { color: var(--accent); }
763 .tree-icon .icon { width: 13px; height: 13px; }
764 .tree-chevron { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); transition: transform 0.12s ease; }
765 .tree-chevron .icon { width: 12px; height: 12px; }
766 details[open] > summary .tree-chevron { transform: rotate(90deg); }
767 .tree-file { padding-left: 25px; }
768 .tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
769 .tree-note, .tree-empty { margin: 0; padding: 8px 10px; font-size: 11px; }
770 .tree-note { border-top: 1px solid var(--border); }
771
772 .code-main .filebody { max-width: none; }
773 .code-main .readme > *,
774 .code-main .filebody > * { max-width: none; }
775 .code-main .highlight,
776 .code-main .blob { max-height: calc(100vh - 230px); }
777 .empty-code { margin-top: 0; }
778
779 .dirlist td { padding: 2px 10px; }
780 .dirlist a { display: inline-flex; align-items: center; gap: 7px; }
781 .dirlist .icon { width: 13px; height: 13px; color: var(--fg-faint); }
782 .dirlist a.dir .icon { color: var(--accent); }
783 .dirlist a.parent { color: var(--fg-muted); }
784
785 @media (max-width: 980px) {
786 .code-browser { grid-template-columns: 220px minmax(0, 1fr); }
787 }
788 @media (max-width: 760px) {
789 .code-browser { grid-template-columns: 1fr; }
790 .file-tree { position: static; max-height: 260px; }
791 .code-main .highlight,
792 .code-main .blob { max-height: none; }
667 793 }
668 .crumb-actions { float: right; display: inline-flex; align-items: baseline; gap: 10px; font-family: var(--font); }
669 .crumb-actions .muted { font-size: 11.5px; }
670 794
671 795 .readme > *,
672 796 .filebody > * { max-width: 84ch; }
@@ -806,6 +930,17 @@ td.fit { width: 1%; white-space: nowrap; }
806 930 margin-bottom: 12px;
807 931 }
808 932 .page-head h1 { margin: 0 0 3px; }
933 .page-head-main { min-width: 0; }
934 .page-head-actions {
935 display: flex;
936 flex-wrap: wrap;
937 flex: 0 0 auto;
938 align-items: center;
939 justify-content: flex-end;
940 gap: 8px;
941 margin-left: auto;
942 }
943 .page-head-actions .clone-pop { left: auto; right: 0; }
809 944 .page-sub { color: var(--fg-muted); font-size: 12.5px; margin: 0; }
810 945
811 946 .state {
@@ -950,6 +1085,7 @@ td.fit { width: 1%; white-space: nowrap; }
950 1085 .thread-side { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 24px; }
951 1086 .thread-side section:first-child { padding-top: 12px; }
952 1087 .page-head { flex-wrap: wrap; }
1088 .page-head-actions { width: 100%; justify-content: flex-start; }
953 1089 }
954 1090
955 1091 /* Form cards: fields live on a card, actions sit below it and align right.