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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Build a landing page and simplify the forge chrome

074ae1605e177e99533af02d8c69ccd1a6a947bb · Keenan Nemetz · 14d ago · parent 62576fa8

Build a landing page and simplify the forge chrome

The instance root is now a landing page rather than the full activity feed:
forge identity, project cards with top language, last commit, issue/change
counts and latest release, a short activity rail, and a first-run empty state.
The full feed moves to /activity with type filters and pagination.

Feeds now cover issues, changes, comments, and releases only; commits and CI
runs already have their own pages. Configuration gains instance_name and
instance_tagline.

Navigation: the crowded global bar becomes Runners, Activity, search, and an
account menu holding Admin, Status, and Log out. Repository pages show the name
and description above the tabs at the home-page header scale, while tab-page
titles stay small. Top-level pages keep the larger header. Drop the workflow
run chip, assigned-issues strip, repository size stat, and footer Status link.

server/web/static/vole.css +297−50

@@ -110,20 +110,84 @@ img { max-width: 100%; }
110 110 font: 700 12px/1 var(--mono);
111 111 }
112 112
113 .topnav { display: flex; gap: 16px; }
114 .topnav a { color: var(--fg-muted); font-size: 12.5px; }
115 .topnav a:hover { color: var(--fg); text-decoration: none; }
113 .topnav { display: flex; gap: 2px; }
114 .topnav a {
115 display: inline-flex;
116 align-items: center;
117 gap: 5px;
118 padding: 4px 8px;
119 border-radius: var(--radius);
120 color: var(--fg-muted);
121 font-size: 12.5px;
122 }
123 .topnav a:hover { background: var(--bg-hover); color: var(--fg); text-decoration: none; }
124 .topnav a.active { background: var(--bg-raised); color: var(--fg); }
125
126 .topsearch { margin-left: auto; }
127 .topsearch input[type="search"] { width: 12rem; max-width: 28vw; }
116 128
117 129 .topright {
118 margin-left: auto;
119 130 display: flex;
120 131 align-items: center;
121 132 gap: 12px;
122 133 font-size: 12.5px;
123 134 color: var(--fg-muted);
124 135 }
125 .topright .account { color: var(--fg-muted); }
126 .topright .account:hover { color: var(--fg); text-decoration: none; }
136 .topright > a { display: inline-flex; align-items: center; gap: 5px; color: var(--fg-muted); }
137 .topright > a:hover { color: var(--fg); text-decoration: none; }
138
139 .account-menu { position: relative; }
140 .account-menu > summary {
141 display: flex;
142 align-items: center;
143 gap: 6px;
144 padding: 4px 8px;
145 border-radius: var(--radius);
146 color: var(--fg-muted);
147 white-space: nowrap;
148 list-style: none;
149 cursor: pointer;
150 }
151 .account-menu > summary::-webkit-details-marker { display: none; }
152 .account-menu > summary:hover { background: var(--bg-hover); color: var(--fg); }
153 .account-menu[open] > summary { background: var(--bg-raised); color: var(--fg); }
154 .account-name { max-width: 12rem; overflow: hidden; text-overflow: ellipsis; }
155 .account-caret { display: inline-flex; color: var(--fg-faint); }
156 .account-caret .icon { width: 12px; height: 12px; transform: rotate(90deg); transition: transform 0.12s ease; }
157 .account-menu[open] .account-caret .icon { transform: rotate(-90deg); }
158 .account-pop {
159 position: absolute;
160 top: calc(100% + 6px);
161 right: 0;
162 z-index: 30;
163 width: 13rem;
164 padding: 4px;
165 background: var(--bg-raised);
166 border: 1px solid var(--border-strong);
167 border-radius: var(--radius-lg);
168 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
169 }
170 .account-pop a,
171 .account-pop button {
172 display: flex;
173 align-items: center;
174 justify-content: flex-start;
175 gap: 8px;
176 width: 100%;
177 padding: 6px 8px;
178 border: 0;
179 border-radius: var(--radius);
180 background: none;
181 color: var(--fg);
182 font: inherit;
183 font-size: 12.5px;
184 text-align: left;
185 cursor: pointer;
186 }
187 .account-pop a:hover,
188 .account-pop button:hover { background: var(--bg-hover); color: var(--accent); text-decoration: none; }
189 .account-pop form { margin: 0; }
190 .account-sep { border-top: 1px solid var(--border); margin: 4px 0; }
127 191
128 192 .page {
129 193 width: min(100%, 1440px);
@@ -133,29 +197,79 @@ img { max-width: 100%; }
133 197
134 198 @media (max-width: 720px) {
135 199 .tabs { mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent); }
136 .topbar { padding: 0 12px; gap: 12px; }
200 .topbar {
201 height: auto;
202 flex-wrap: wrap;
203 padding: 8px 12px;
204 gap: 8px 12px;
205 }
206 .topsearch { order: 3; flex: 1 1 100%; margin-left: 0; }
207 .topsearch input[type="search"] { width: 100%; max-width: none; }
208 .topright { margin-left: auto; }
209 .topnav { gap: 0; }
210 /* The icon is enough on a phone; the menu keeps the full name. */
211 .account-name { display: none; }
137 212 .page { padding: 16px 12px 56px; }
138 .topnav { gap: 10px; }
139 213 }
140 214
141 215 /* ---------- repository header ---------- */
142 216
143 217 .repohead {
144 display: flex;
145 flex-wrap: wrap;
146 align-items: baseline;
147 gap: 6px 16px;
148 padding-bottom: 0;
149 218 margin-bottom: 16px;
150 219 border-bottom: 1px solid var(--border);
151 220 }
221 .repo-title { padding: 26px 0 12px; }
222 .repo-name {
223 display: flex;
224 align-items: center;
225 gap: 8px;
226 min-width: 0;
227 color: var(--fg);
228 font-size: 26px;
229 line-height: 1.2;
230 font-weight: 600;
231 letter-spacing: -0.025em;
232 }
233 .repo-name .repo-vis .icon { width: 16px; height: 16px; }
234 .repo-desc {
235 margin: 6px 0 0;
236 color: var(--fg-muted);
237 font-size: 13px;
238 overflow: hidden;
239 text-overflow: ellipsis;
240 white-space: nowrap;
241 }
242
243 /* The tab pages are a level below the repository identity: their own headers
244 stay small. Top-level pages (Runners, Account, Status, Admin) keep the
245 home-page header scale. */
246 .repohead + .page-head {
247 align-items: flex-start;
248 gap: 16px;
249 padding: 0;
250 margin-bottom: 12px;
251 border-bottom: 0;
252 }
253 .repohead + .page-head h1 {
254 font-size: 18px;
255 line-height: 1.35;
256 letter-spacing: -0.005em;
257 margin: 0 0 3px;
258 }
259 .repohead + .page-head .page-sub { font-size: 12.5px; }
260
261 @media (max-width: 720px) {
262 .repo-title { padding: 18px 0 10px; }
263 .repo-name { font-size: 22px; }
264 .repo-name .repo-vis .icon { width: 14px; height: 14px; }
265 }
152 266
153 267 .tabs {
154 268 display: flex;
155 269 gap: 2px;
156 270 overflow-x: auto;
157 271 scrollbar-width: none;
158 margin-left: -8px;
272 margin-left: -10px;
159 273 }
160 274 .tabs::-webkit-scrollbar { display: none; }
161 275 .tabs a {
@@ -167,7 +281,6 @@ img { max-width: 100%; }
167 281 }
168 282 .tabs a:hover { color: var(--fg); text-decoration: none; }
169 283 .tabs a.active { color: var(--fg); border-bottom-color: var(--accent); }
170 .tabs a.repotab { color: var(--fg); font-weight: 600; }
171 284
172 285 /* ---------- panels and tables ---------- */
173 286
@@ -711,6 +824,7 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; }
711 824 .chip {
712 825 display: inline-flex;
713 826 align-items: center;
827 gap: 5px;
714 828 padding: 3px 9px;
715 829 border: 1px solid var(--border-strong);
716 830 border-radius: 12px;
@@ -718,6 +832,7 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; }
718 832 color: var(--fg-muted);
719 833 background: var(--bg-panel);
720 834 }
835 .chip .icon { width: 12px; height: 12px; }
721 836 .chip:hover { color: var(--fg); border-color: var(--fg-faint); text-decoration: none; }
722 837 .chip.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
723 838
@@ -895,7 +1010,7 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); }
895 1010 .code-main .readme > *,
896 1011 .code-main .filebody > * { max-width: none; }
897 1012 .code-main .highlight,
898 .code-main .blob { max-height: calc(100vh - 230px); }
1013 .code-main .blob { max-height: calc(100vh - 240px); }
899 1014 .empty-code { margin-top: 0; }
900 1015
901 1016 .dirlist td { padding: 2px 10px; }
@@ -1040,18 +1155,24 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); }
1040 1155 }
1041 1156 .side-run-top { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
1042 1157 .side-run-top .status { flex: 0 0 auto; }
1043 .side-size { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 2px; }
1044 1158
1045 1159 /* ---------- issues and changes ---------- */
1046 1160
1047 1161 .page-head {
1048 1162 display: flex;
1049 align-items: flex-start;
1163 align-items: flex-end;
1050 1164 justify-content: space-between;
1051 gap: 16px;
1052 margin-bottom: 12px;
1165 gap: 24px;
1166 padding: 26px 0 20px;
1167 margin-bottom: 20px;
1168 border-bottom: 1px solid var(--border);
1169 }
1170 .page-head h1 {
1171 font-size: 26px;
1172 line-height: 1.2;
1173 letter-spacing: -0.025em;
1174 margin: 0 0 4px;
1053 1175 }
1054 .page-head h1 { margin: 0 0 3px; }
1055 1176 .page-head-main { min-width: 0; }
1056 1177 .page-head-actions {
1057 1178 display: flex;
@@ -1063,7 +1184,12 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); }
1063 1184 margin-left: auto;
1064 1185 }
1065 1186 .page-head-actions .clone-pop { left: auto; right: 0; }
1066 .page-sub { color: var(--fg-muted); font-size: 12.5px; margin: 0; }
1187 .page-sub { color: var(--fg-muted); font-size: 13px; margin: 0; max-width: 80ch; }
1188
1189 @media (max-width: 720px) {
1190 .page-head { padding: 18px 0 16px; }
1191 .page-head h1 { font-size: 22px; }
1192 }
1067 1193
1068 1194 /* Secondary navigation inside a change: conversation and the review files. */
1069 1195 .subtabs {
@@ -1546,7 +1672,6 @@ button.link, .link { min-height: 0; }
1546 1672 /* Icons are inline SVG from Lucide. They inherit currentColor and size with
1547 1673 the text around them. */
1548 1674 .icon { width: 14px; height: 14px; flex: 0 0 auto; }
1549 .topnav a, .topright .account { display: inline-flex; align-items: center; gap: 5px; }
1550 1675 .tabs a { display: inline-flex; align-items: center; gap: 5px; }
1551 1676 h2 .icon { width: 13px; height: 13px; margin-right: 4px; vertical-align: -2px; color: var(--fg-faint); }
1552 1677
@@ -1610,11 +1735,9 @@ form.htmx-request .label-tokens { opacity: 0.6; }
1610 1735 color: var(--fg-muted);
1611 1736 }
1612 1737 .activity-icon .icon { width: 13px; height: 13px; }
1613 .activity-icon.commit { color: var(--accent); }
1614 1738 .activity-icon.issue { color: var(--good); }
1615 1739 .activity-icon.change { color: #c792ea; }
1616 1740 .activity-icon.release { color: var(--warn); }
1617 .activity-icon.run { color: var(--fg-muted); }
1618 1741 .activity-body { min-width: 0; flex: 1; }
1619 1742 .activity-title {
1620 1743 display: block;
@@ -1634,16 +1757,6 @@ form.htmx-request .label-tokens { opacity: 0.6; }
1634 1757 .state.active { color: var(--good); }
1635 1758 .state.retired { color: var(--fg-faint); }
1636 1759
1637 /* Global repository search lives in the chrome on wide screens; the index page
1638 keeps its own visible form so the control is still reachable on mobile. */
1639 .topsearch { margin-left: auto; }
1640 .topsearch input[type="search"] { width: 12rem; max-width: 28vw; }
1641 .topright { margin-left: 0; }
1642 @media (max-width: 720px) {
1643 .topsearch { display: none; }
1644 .topright { margin-left: auto; }
1645 }
1646
1647 1760 /* Keyboard users get out of the chrome before the tabs. */
1648 1761 .skip-link {
1649 1762 position: absolute;
@@ -1731,20 +1844,135 @@ textarea:focus-visible {
1731 1844 .blame td.ln { width: 6ch; text-align: right; color: var(--fg-faint); font-family: var(--mono); font-size: 11px; }
1732 1845 .blame td.code { font-family: var(--mono); font-size: 12px; white-space: pre; }
1733 1846
1734 /* Home: the instance feed is the main column and the repository list is a
1735 compact index beside it. On narrow screens the list comes first. */
1736 .home { display: grid; grid-template-columns: minmax(0, 1fr) 20rem; gap: 24px; align-items: start; }
1737 .home-side { position: sticky; top: 60px; }
1738 .home-side h2 { margin-top: 0; }
1739 .side-search { margin: 0 0 8px; }
1740 .side-search input[type="search"] { width: 100%; max-width: none; }
1741 .repo-side .side-row { display: block; padding: 7px 0; border-bottom: 1px solid var(--border); }
1742 .repo-side .side-row:last-child { border-bottom: 0; }
1743 .repo-side .row-title { display: block; }
1744 .repo-side .muted { display: block; font-size: 11.5px; margin-top: 1px; }
1847 /* Landing page: identity, a project grid, and the merged feed as a rail. The
1848 feed has a full page at /activity; here it is a sign the forge is alive, not
1849 the subject of the page. */
1850 .hero {
1851 display: flex;
1852 align-items: flex-end;
1853 justify-content: space-between;
1854 gap: 24px;
1855 padding: 26px 0 20px;
1856 border-bottom: 1px solid var(--border);
1857 margin-bottom: 20px;
1858 }
1859 .hero-main { min-width: 0; }
1860 .hero h1 {
1861 font-size: 26px;
1862 line-height: 1.2;
1863 letter-spacing: -0.025em;
1864 margin: 0 0 6px;
1865 }
1866 .hero-tagline { color: var(--fg-muted); font-size: 13px; margin: 0 0 8px; max-width: 62ch; }
1867 .hero-vitals { color: var(--fg-faint); font: 11.5px/1.5 var(--mono); margin: 0; }
1868 .hero-vitals a { color: var(--fg-muted); }
1869 .hero-actions { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; }
1870
1871 .home-grid {
1872 display: grid;
1873 grid-template-columns: minmax(0, 1fr) 20rem;
1874 gap: 28px;
1875 align-items: start;
1876 }
1877 .home-main { min-width: 0; }
1878 .home-rail { position: sticky; top: 60px; }
1879 .section-head {
1880 display: flex;
1881 align-items: baseline;
1882 justify-content: space-between;
1883 gap: 12px;
1884 margin-bottom: 9px;
1885 }
1886 .section-head h2 { margin: 0; }
1887 .section-head .more { font-size: 11.5px; color: var(--fg-muted); }
1888 .section-head .more:hover { color: var(--accent); }
1889 .section-head .muted { font-size: 11.5px; }
1890
1891 .repo-grid {
1892 display: grid;
1893 grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
1894 gap: 10px;
1895 }
1896 .repo-card {
1897 display: flex;
1898 flex-direction: column;
1899 gap: 5px;
1900 min-width: 0;
1901 padding: 12px 13px;
1902 background: var(--bg-panel);
1903 border: 1px solid var(--border);
1904 border-radius: var(--radius-lg);
1905 }
1906 .repo-card:hover { border-color: var(--border-strong); background: var(--bg-raised); }
1907 .repo-card-top { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
1908 .repo-card h3 { font-size: 13.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1909 .repo-card h3 a { color: var(--fg); font-weight: 600; }
1910 .repo-card h3 a:hover { color: var(--accent); text-decoration: none; }
1911 .repo-vis { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); }
1912 .repo-vis .icon { width: 12px; height: 12px; }
1913 .repo-lang { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; font-size: 10.5px; color: var(--fg-faint); }
1914 .lang-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--fg-faint); }
1915 .repo-updated { margin-left: auto; flex: 0 0 auto; color: var(--fg-faint); font-size: 11px; }
1916 .repo-desc {
1917 color: var(--fg-muted);
1918 font-size: 12px;
1919 margin: 0;
1920 overflow: hidden;
1921 display: -webkit-box;
1922 -webkit-line-clamp: 2;
1923 -webkit-box-orient: vertical;
1924 }
1925 .repo-meta {
1926 display: flex;
1927 flex-wrap: wrap;
1928 gap: 2px 8px;
1929 margin: 0;
1930 color: var(--fg-faint);
1931 font-size: 11px;
1932 min-width: 0;
1933 }
1934 .repo-meta .repo-subject { color: var(--fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
1935 .repo-meta .sha { font-size: 11px; color: var(--fg-faint); }
1936 .repo-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 3px; }
1937 .repo-chips .chip { padding: 1px 7px; font-size: 10.5px; gap: 4px; }
1938 .repo-chips .chip .icon { width: 11px; height: 11px; }
1939 .repo-chips .status { font-size: 9.5px; letter-spacing: 0.03em; }
1940 .repo-chips .status::before { width: 5px; height: 5px; }
1941
1942 .empty-state.first-run {
1943 display: flex;
1944 flex-direction: column;
1945 align-items: center;
1946 text-align: center;
1947 padding: 44px 20px;
1948 }
1949 .first-run .empty-icon {
1950 display: grid;
1951 place-items: center;
1952 width: 44px;
1953 height: 44px;
1954 margin-bottom: 12px;
1955 border-radius: 50%;
1956 background: var(--bg-raised);
1957 color: var(--fg-faint);
1958 }
1959 .first-run .empty-icon .icon { width: 20px; height: 20px; }
1960 .first-run h2 { margin: 0 0 6px; font-size: 16px; color: var(--fg); }
1961 .first-run p { margin: 0 0 14px; }
1962 .first-run .empty-actions { margin: 0 0 10px; }
1963 .first-run .empty-hint { margin: 0; font-size: 11.5px; color: var(--fg-faint); }
1964 .first-run .empty-hint code { font-size: 11px; }
1965
1745 1966 @media (max-width: 900px) {
1746 .home { grid-template-columns: 1fr; }
1747 .home-side { position: static; order: -1; }
1967 .home-grid { grid-template-columns: 1fr; }
1968 .home-rail { position: static; order: 2; }
1969 .home-main { order: 1; }
1970 }
1971 @media (max-width: 720px) {
1972 .hero { flex-wrap: wrap; align-items: flex-start; padding-top: 18px; }
1973 .hero-actions { width: 100%; }
1974 .hero h1 { font-size: 22px; }
1975 .repo-grid { grid-template-columns: 1fr; }
1748 1976 }
1749 1977
1750 1978 /* Status page: a small metrics row above the per-service checks. */
@@ -1755,8 +1983,6 @@ textarea:focus-visible {
1755 1983 .check-state.ok { color: var(--good); }
1756 1984 .check-state.warn { color: var(--warn); }
1757 1985 .check-state.fail { color: var(--bad); }
1758 .pagefoot { max-width: 1440px; margin: 0 auto; padding: 0 24px 32px; color: var(--fg-faint); font-size: 11.5px; }
1759 @media (max-width: 720px) { .pagefoot { padding: 0 12px 24px; } }
1760 1986
1761 1987 /* Language hints for file rows. Active tree rows still win through specificity. */
1762 1988 .tree-icon.lang-go, .dirlist a.file.lang-go .icon { color: #00add8; }
@@ -1779,3 +2005,24 @@ textarea:focus-visible {
1779 2005 .tree-icon.lang-image, .dirlist a.file.lang-image .icon { color: #a074c4; }
1780 2006 .tree-icon.lang-lock, .dirlist a.file.lang-lock .icon { color: var(--fg-muted); }
1781 2007 .tree-icon.lang-build, .dirlist a.file.lang-build .icon { color: var(--fg-muted); }
2008
2009 /* The same palette marks a repository's top language on the landing page. */
2010 .lang-dot.lang-go { background: #00add8; }
2011 .lang-dot.lang-rust { background: #dea584; }
2012 .lang-dot.lang-js { background: #f1e05a; }
2013 .lang-dot.lang-ts { background: #3178c6; }
2014 .lang-dot.lang-py { background: #3572a5; }
2015 .lang-dot.lang-rb { background: #cc342d; }
2016 .lang-dot.lang-jvm { background: #b07219; }
2017 .lang-dot.lang-c { background: #a8b9cc; }
2018 .lang-dot.lang-cpp { background: #f34b7d; }
2019 .lang-dot.lang-cs { background: #178600; }
2020 .lang-dot.lang-php { background: #8f9ad8; }
2021 .lang-dot.lang-sh { background: #89e051; }
2022 .lang-dot.lang-html { background: #e34c26; }
2023 .lang-dot.lang-css { background: #a074c4; }
2024 .lang-dot.lang-config { background: #cbcb41; }
2025 .lang-dot.lang-text { background: #519aba; }
2026 .lang-dot.lang-sql { background: #e38c00; }
2027 .lang-dot.lang-image { background: #a074c4; }
2028 .lang-dot.lang-lock, .lang-dot.lang-build { background: var(--fg-muted); }