From dacca5a820d372be31d9058ab209055921e63e70 Mon Sep 17 00:00:00 2001 From: gl0w Date: Thu, 10 Sep 2026 08:39:46 +0100 Subject: [PATCH] m6/no-scroll: stacked cards on mobile, tables on desktop --- src/build.rs | 39 +++++++++++++--- src/commits.rs | 34 ++++++++++++-- src/forge.rs | 100 +++++++++++++++++++++++++++++++++++++++-- src/releases.rs | 46 ++++++++++++++++++- static/style.css | 21 +++++++++ tests/forge_readers.rs | 10 ++--- tests/releases_api.rs | 10 ++--- tests/responsive.rs | 8 ++-- 8 files changed, 238 insertions(+), 30 deletions(-) diff --git a/src/build.rs b/src/build.rs index d70dc3f..b0b69ca 100644 --- a/src/build.rs +++ b/src/build.rs @@ -584,8 +584,8 @@ pub fn file_table_html( ); if let Some((href, label)) = parent { out.push_str(&format!( - "{}\ - \n", + "{}\ + \n", esc(href), esc(label) )); @@ -600,8 +600,8 @@ pub fn file_table_html( let msg_short = truncate_msg(&msg); if e.is_dir { out.push_str(&format!( - "{}/\ - {}{}\n", + "{}/\ + {}{}\n", href_esc(&full), esc(&e.name), esc(&msg_short), @@ -609,8 +609,8 @@ pub fn file_table_html( )); } else { out.push_str(&format!( - "{}\ - {}{}\n", + "{}\ + {}{}\n", href_esc(&full), esc(&e.name), esc(&msg_short), @@ -1651,7 +1651,32 @@ mod tests { "breadcrumb kept on subdir" ); // Message + age columns exist even when history is empty-ish. - assert!(sub.contains(""), "message cell"); + assert!(sub.contains(" 0, "{page} has cells"); + assert_eq!( + html.matches("data-label=").count(), + tds, + "{page}: every td labeled" + ); + for label in ["Name", "Last commit", "Age"] { + assert!( + html.contains(&format!("data-label=\"{label}\"")), + "{page}: label {label}" + ); + } + } } #[test] diff --git a/src/commits.rs b/src/commits.rs index d5fa33e..7075113 100644 --- a/src/commits.rs +++ b/src/commits.rs @@ -40,7 +40,7 @@ pub fn render_loaded( body.push_str("
\n\n\n"); for c in log { body.push_str(&format!( - "\n", + "\n", esc(&c.sha), esc(short_sha(&c.sha)), esc(&c.subject), @@ -342,8 +342,8 @@ mod tests { list.contains("
shasubjectauthordate
{}{}{}{}
{}{}{}{}
"), "wrapper" ); - assert!(list.contains("
"), "title cell"); - assert!(list.contains(""), "sha cell"); + assert!(list.contains("", None).expect("render"); + let list = std::fs::read_to_string(out.join("commits.html")).expect("list"); + let tds = list.matches(" 0, "has cells"); + assert_eq!(list.matches("data-label=").count(), tds, "every td labeled"); + for label in ["sha", "subject", "author", "date"] { + assert!( + list.contains(&format!("data-label=\"{label}\"")), + "label {label}" + ); + } + } } diff --git a/src/forge.rs b/src/forge.rs index fe2c0e4..b41ed7a 100644 --- a/src/forge.rs +++ b/src/forge.rs @@ -986,7 +986,7 @@ fn render_issues_index( body.push_str("
\n\n\n"); for issue in &data.issues { body.push_str(&format!( - "\n", + "\n", n = issue.number, id = esc(&issue.id), title = esc(&issue.title), @@ -1045,7 +1045,7 @@ fn render_pulls_index( let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base)); let draft = if pr.draft { " draft" } else { "" }; body.push_str(&format!( - "\n", + "\n", n = pr.number, id = esc(&pr.id), title = esc(&pr.title), @@ -1143,7 +1143,7 @@ fn render_ci_index(ci: &CiInfo, site_name: &str, nav: &str, description: Option< body.push_str("
#titlestatecommentscreated
{n}{title}{state}{comments}{created}
{n}{title}{state}{comments}{created}
{n}{title}{draft}{state}{lanes}{created}
{n}{title}{draft}{state}{lanes}{created}
\n\n\n"); for s in &run.steps { body.push_str(&format!( - "\n", + "\n", esc(&s.name), s.exit, s.duration_ms, @@ -1690,7 +1690,7 @@ mod tests { html.contains("
stepexitduration_ms
{}{}{}
{}{}{}
"), "{page}: wrapper" ); - assert!(html.contains("
"), "{page}: title cell"); + assert!(html.contains("", None).expect("render"); + for (page, labels) in [ + ( + "issues/index.html", + vec!["#", "title", "state", "comments", "created"], + ), + ( + "pulls/index.html", + vec!["#", "title", "state", "head→base", "created"], + ), + ("ci/index.html", vec!["step", "exit", "duration_ms"]), + ] { + let html = std::fs::read_to_string(out.join(page)).expect(page); + let tds = html.matches(" 0, "{page} has cells"); + assert_eq!( + html.matches("data-label=").count(), + tds, + "{page}: every td labeled" + ); + for label in labels { + assert!( + html.contains(&format!("data-label=\"{label}\"")), + "{page}: label {label}" + ); + } + } + let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks"); + assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper"); + assert!(tasks.contains("todo (1)"), "tasks group kept"); + let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues"); + assert!( + issues.contains("open") && issues.contains("2026-09-10"), + "issues data kept in DOM" + ); + } } diff --git a/src/releases.rs b/src/releases.rs index ad238d9..87493bb 100644 --- a/src/releases.rs +++ b/src/releases.rs @@ -391,7 +391,7 @@ pub fn render_loaded( body.push_str("
\n\n\n"); for rel in releases { body.push_str(&format!( - "\n", + "\n", v = esc(&rel.version), ve = esc(&rel.version), t = esc(rel.display_title()), @@ -493,7 +493,7 @@ mod tests { index.contains("
versiontitlecreatedartifacts
{ve}{t}{c}{n}
{ve}{t}{c}{n}
"), "wrapper" ); - assert!(index.contains(""), + html.contains("
"), "title cell"); + assert!(index.contains("", None).expect("render"); + let index = + std::fs::read_to_string(out.join("releases").join("index.html")).expect("index"); + let tds = index.matches(" 0, "has cells"); + assert_eq!( + index.matches("data-label=").count(), + tds, + "every td labeled" + ); + for label in ["version", "title", "created", "artifacts"] { + assert!( + index.contains(&format!("data-label=\"{label}\"")), + "label {label}" + ); + } + } } diff --git a/static/style.css b/static/style.css index a09ecf2..2efee6d 100644 --- a/static/style.css +++ b/static/style.css @@ -183,3 +183,24 @@ details.clonebox{margin:1rem 0;padding:.25rem 1rem 1rem;background:var(--bg-alt) details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan)} details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px} .clone-hint,.clonebox summary .muted{font-weight:400} +/* Stacked cards on narrow (m6/no-scroll, f0rge #39): Gitea-style reflow. + Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above. + Narrow: no scroll (overflow visible), thead clipped for SR only, each + tr becomes a bordered card, each td a labeled flex row via data-label. + Title cells lead bold without a repeated label; nothing is deleted. */ +@media (max-width:767px){ + .table-scroll{overflow-x:visible} + .table-scroll table,.file-table table{width:100%;min-width:0} + .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap} + .table-scroll tbody,.table-scroll tr{display:block;width:auto} + .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0} + .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal} + .table-scroll td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere} + .table-scroll td:last-child{border-bottom:none} + .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere} + .table-scroll td.title::before{display:none} + .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0} + .table-scroll td.num,.table-scroll td.sha{width:auto} + .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0} + .table-scroll tbody tr:hover td{background:none} +} diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs index d695600..b3aa723 100644 --- a/tests/forge_readers.rs +++ b/tests/forge_readers.rs @@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() { // --- issues index: 2 rendered rows (broken one skipped) --- let issues = read("issues/index.html"); - let rows = issues.matches("
").count(); + let rows = issues.matches("
1" + "1" )), "issue #1 -> older uuid" ); assert!( issues.contains(&format!( - "2" + "2" )), "issue #2 -> newer uuid" ); @@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() { // (4): pulls — same shape + head/base/draft/ci + patch
.
     let pulls = read("pulls/index.html");
     assert_eq!(
-        pulls.matches("
").count(), + pulls.matches("
1" + "1" )), "pr #1 -> uuid" ); diff --git a/tests/releases_api.rs b/tests/releases_api.rs index 9ea9a71..ea32b2c 100644 --- a/tests/releases_api.rs +++ b/tests/releases_api.rs @@ -110,7 +110,7 @@ fn releases_artifacts_api_agents_feed() { // --- releases: index + both detail pages --- let index = read("releases/index.html"); - assert_eq!(index.matches("
").count(), 2, "two release rows"); + assert_eq!(index.matches("
") + .matches("
") + .matches("
") + .matches("
").count(); + let releases_html = read("releases/index.html").matches("
"), "{page} keeps # narrow"); + assert!(html.contains(""), + commits.contains(""), + releases.contains("