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. | |