fabrica

hanna/fabrica

feat(web): paginate dashboard activity; label + language-bar fixes

d1603bb · hanna committed on 2026-07-25

Paginate the dashboard "Recent activity" feed (retain up to 100 commits, 10 per
page with prev/next + a page-size selector that preserves the repo-search term).
Lay label descriptions to the right of the chip instead of below. Fully hide the
language-bar <details> disclosure triangle (display:block on the summary) so it
no longer shows or reserves left indent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +37 −11UnifiedSplit
assets/base.css +13 −3
@@ -342,9 +342,13 @@ body.drawer-open .drawer-backdrop {
342342 }
343343 .label-list .entry-row-body {
344344 display: flex;
345- flex-direction: column;
346- gap: 0.25rem;
347- align-items: flex-start;
345+ flex-direction: row;
346+ align-items: center;
347+ gap: 0.6rem;
348+ flex-wrap: wrap;
349+}
350+.label-list .entry-row-meta {
351+ margin: 0;
348352 }
349353 .label-add {
350354 display: flex;
@@ -606,12 +610,18 @@ body.drawer-open .drawer-backdrop {
606610 margin: 0 0 1rem;
607611 }
608612 .lang-bar-summary {
613+ /* `display: block` drops the default disclosure triangle (and the indent it
614+ reserves) in Chromium; the list-style/marker resets cover other engines. */
615+ display: block;
609616 list-style: none;
610617 cursor: pointer;
611618 }
612619 .lang-bar-summary::-webkit-details-marker {
613620 display: none;
614621 }
622+.lang-bar-summary::marker {
623+ content: "";
624+}
615625 .lang-bar {
616626 display: flex;
617627 height: 0.6rem;
crates/web/src/activity.rs +20 −5
@@ -25,8 +25,8 @@ use crate::repo::git_read;
2525 const WEEKS: usize = 53;
2626 /// Commits scanned per repo on its default branch — bounds dashboard cost.
2727 const SCAN_PER_REPO: usize = 400;
28-/// Recent commits shown in the activity feed.
29-const FEED_LIMIT: usize = 20;
28+/// The most recent commits retained for the (paginated) activity feed.
29+const FEED_LIMIT: usize = 100;
3030
3131 /// One commit in the activity feed.
3232 pub struct FeedItem {
@@ -219,8 +219,20 @@ impl Activity {
219219 }
220220 }
221221
222- /// Render the recent-activity commit feed.
223- pub fn feed_section(&self, now_ms: i64) -> Markup {
222+ /// Render the recent-activity commit feed, paginated. `path`/`query` are the
223+ /// dashboard URL and its query (so the page-size selector and prev/next keep
224+ /// any repo-search term).
225+ pub fn feed_section(
226+ &self,
227+ now_ms: i64,
228+ paging: crate::repo::Pagination,
229+ path: &str,
230+ query: Option<&str>,
231+ ) -> Markup {
232+ let start = paging.offset().min(self.feed.len());
233+ let end = start.saturating_add(paging.per_page).min(self.feed.len());
234+ let page = &self.feed[start..end];
235+ let has_next = end < self.feed.len();
224236 html! {
225237 section class="listing" {
226238 h2 { "Recent activity" }
@@ -228,7 +240,7 @@ impl Activity {
228240 div class="card" { p class="muted" { "No recent activity." } }
229241 } @else {
230242 ul class="activity-feed card" {
231- @for item in &self.feed {
243+ @for item in page {
232244 @let base = format!("/{}/{}", item.owner, item.repo);
233245 li class="activity-item" {
234246 div class="activity-head muted" {
@@ -246,6 +258,9 @@ impl Activity {
246258 }
247259 }
248260 }
261+ @if has_next || paging.page > 1 {
262+ (crate::repo::pagination_nav(path, query, paging, has_next))
263+ }
249264 }
250265 }
251266 }
crates/web/src/pages.rs +4 −3
@@ -70,7 +70,7 @@ pub async fn home(
7070 ) -> AppResult<Response> {
7171 let (jar, chrome) = build_chrome(&state, jar, user.clone(), uri.path().to_string());
7272 if let Some(u) = user {
73- let body = dashboard_body(&state, &u, &lq.q.unwrap_or_default()).await?;
73+ let body = dashboard_body(&state, &u, &lq.q.unwrap_or_default(), &uri).await?;
7474 Ok((jar, page(&chrome, "Dashboard", body)).into_response())
7575 } else {
7676 let body = explore_repos_body(&state, "").await?;
@@ -217,9 +217,10 @@ fn user_card(users: &[User]) -> Markup {
217217
218218 /// The dashboard: the viewer's contribution activity and recent commits, beside
219219 /// a filterable list of their repositories.
220-async fn dashboard_body(state: &AppState, user: &User, q: &str) -> AppResult<Markup> {
220+async fn dashboard_body(state: &AppState, user: &User, q: &str, uri: &Uri) -> AppResult<Markup> {
221221 let now = now_ms();
222222 let activity = crate::activity::compute(state, user, now).await?;
223+ let paging = repo::Pagination::from_query(uri.query());
223224
224225 let mine = state.store.repos_by_owner(&user.id).await?;
225226 let needle = q.trim().to_lowercase();
@@ -233,7 +234,7 @@ async fn dashboard_body(state: &AppState, user: &User, q: &str) -> AppResult<Mar
233234 div class="dashboard" {
234235 div class="dashboard-main" {
235236 (activity.heatmap())
236- (activity.feed_section(now))
237+ (activity.feed_section(now, paging, uri.path(), uri.query()))
237238 }
238239 aside class="dashboard-side" {
239240 form class="search-row" method="get" {