#35 m6/no-scroll
Patch
src/build.rs+32 −7--- a/src/build.rs+++ b/src/build.rs@@ -584,8 +584,8 @@ pub fn file_table_html(584584 );585585 if let Some((href, label)) = parent {586586 out.push_str(&format!(587- "<tr class=\"parent\"><td><a href=\"{}\">{}</a></td>\588- <td></td><td></td></tr>\n",587+ "<tr class=\"parent\"><td data-label=\"Name\"><a href=\"{}\">{}</a></td>\588+ <td data-label=\"Last commit\"></td><td data-label=\"Age\"></td></tr>\n",589589 esc(href),590590 esc(label)591591 ));@@ -600,8 +600,8 @@ pub fn file_table_html(600600 let msg_short = truncate_msg(&msg);601601 if e.is_dir {602602 out.push_str(&format!(603- "<tr class=\"dir\"><td><a href=\"/files/{}.html\">{}/</a></td>\604- <td class=\"title\">{}</td><td>{}</td></tr>\n",603+ "<tr class=\"dir\"><td data-label=\"Name\"><a href=\"/files/{}.html\">{}/</a></td>\604+ <td class=\"title\" data-label=\"Last commit\">{}</td><td data-label=\"Age\">{}</td></tr>\n",605605 href_esc(&full),606606 esc(&e.name),607607 esc(&msg_short),@@ -609,8 +609,8 @@ pub fn file_table_html(609609 ));610610 } else {611611 out.push_str(&format!(612- "<tr class=\"file\"><td><a href=\"/files/{}.html\">{}</a></td>\613- <td class=\"title\">{}</td><td>{}</td></tr>\n",612+ "<tr class=\"file\"><td data-label=\"Name\"><a href=\"/files/{}.html\">{}</a></td>\613+ <td class=\"title\" data-label=\"Last commit\">{}</td><td data-label=\"Age\">{}</td></tr>\n",614614 href_esc(&full),615615 esc(&e.name),616616 esc(&msg_short),@@ -1651,7 +1651,32 @@ mod tests {16511651 "breadcrumb kept on subdir"16521652 );16531653 // Message + age columns exist even when history is empty-ish.1654- assert!(sub.contains("<td class=\"title\">"), "message cell");1654+ assert!(sub.contains("<td class=\"title\""), "message cell");1655+ }1656+1657+ /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.1658+ #[test]1659+ fn file_table_cells_have_data_labels() {1660+ let css = include_str!("../static/style.css");1661+ assert!(css.contains("content:attr(data-label)"), "label rule");1662+ assert!(css.contains("@media (max-width:767px)"), "narrow query");1663+ let (_repo, out) = build_fixture();1664+ for page in ["index.html", "files/sub.html"] {1665+ let html = std::fs::read_to_string(out.join(page)).expect(page);1666+ let tds = html.matches("<td").count();1667+ assert!(tds > 0, "{page} has cells");1668+ assert_eq!(1669+ html.matches("data-label=").count(),1670+ tds,1671+ "{page}: every td labeled"1672+ );1673+ for label in ["Name", "Last commit", "Age"] {1674+ assert!(1675+ html.contains(&format!("data-label=\"{label}\"")),1676+ "{page}: label {label}"1677+ );1678+ }1679+ }16551680 }16561681 16571682 #[test]
src/commits.rs+31 −3--- a/src/commits.rs+++ b/src/commits.rs@@ -40,7 +40,7 @@ pub fn render_loaded(4040 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>sha</th><th>subject</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");4141 for c in log {4242 body.push_str(&format!(43- "<tr><td class=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\">{}</td><td>{}</td><td>{}</td></tr>\n",43+ "<tr><td class=\"sha\" data-label=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\" data-label=\"subject\">{}</td><td data-label=\"author\">{}</td><td data-label=\"date\">{}</td></tr>\n",4444 esc(&c.sha),4545 esc(short_sha(&c.sha)),4646 esc(&c.subject),@@ -342,8 +342,8 @@ mod tests {342342 list.contains("<div class=\"table-scroll\"><table>"),343343 "wrapper"344344 );345- assert!(list.contains("<td class=\"title\">"), "title cell");346- assert!(list.contains("<td class=\"sha\">"), "sha cell");345+ assert!(list.contains("<td class=\"title\""), "title cell");346+ assert!(list.contains("<td class=\"sha\""), "sha cell");347347 assert!(list.contains(&long_subject), "long subject kept");348348 let sha = shas(&dir)[0].clone();349349 assert!(@@ -368,4 +368,32 @@ mod tests {368368 let list = std::fs::read_to_string(out.join("commits.html")).expect("list");369369 assert!(list.contains("No commits yet."));370370 }371+372+ /// m6/no-scroll (f0rge #39): commit rows carry data-labels for cards.373+ #[test]374+ fn commit_cells_have_data_labels() {375+ let css = include_str!("../static/style.css");376+ assert!(css.contains("content:attr(data-label)"), "label rule");377+ assert!(css.contains("@media (max-width:767px)"), "narrow query");378+ let dir = mktemp("labels");379+ git(&dir, &["init", "-b", "main"]);380+ git(&dir, &["config", "user.name", "hu13-test"]);381+ git(&dir, &["config", "user.email", "[email protected]"]);382+ git(&dir, &["config", "commit.gpgsign", "false"]);383+ std::fs::write(dir.join("x.txt"), "x\n").unwrap();384+ git(&dir, &["add", "-A"]);385+ git(&dir, &["commit", "-m", "labeled subject"]);386+ let out = mktemp("out");387+ render(&dir, &out, "main", 100, "test", "<nav>", None).expect("render");388+ let list = std::fs::read_to_string(out.join("commits.html")).expect("list");389+ let tds = list.matches("<td").count();390+ assert!(tds > 0, "has cells");391+ assert_eq!(list.matches("data-label=").count(), tds, "every td labeled");392+ for label in ["sha", "subject", "author", "date"] {393+ assert!(394+ list.contains(&format!("data-label=\"{label}\"")),395+ "label {label}"396+ );397+ }398+ }371399 }
src/forge.rs+96 −4--- a/src/forge.rs+++ b/src/forge.rs@@ -986,7 +986,7 @@ fn render_issues_index(986986 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");987987 for issue in &data.issues {988988 body.push_str(&format!(989- "<tr><td class=\"num\">{n}</td><td class=\"title\"><a href=\"/issues/{id}.html\">{title}</a></td><td>{state}</td><td>{comments}</td><td>{created}</td></tr>\n",989+ "<tr><td class=\"num\" data-label=\"#\">{n}</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{id}.html\">{title}</a></td><td data-label=\"state\">{state}</td><td data-label=\"comments\">{comments}</td><td data-label=\"created\">{created}</td></tr>\n",990990 n = issue.number,991991 id = esc(&issue.id),992992 title = esc(&issue.title),@@ -1045,7 +1045,7 @@ fn render_pulls_index(10451045 let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));10461046 let draft = if pr.draft { " draft" } else { "" };10471047 body.push_str(&format!(1048- "<tr><td class=\"num\">{n}</td><td class=\"title\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td>{state}</td><td><code>{lanes}</code></td><td>{created}</td></tr>\n",1048+ "<tr><td class=\"num\" data-label=\"#\">{n}</td><td class=\"title\" data-label=\"title\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td data-label=\"state\">{state}</td><td data-label=\"head→base\"><code>{lanes}</code></td><td data-label=\"created\">{created}</td></tr>\n",10491049 n = pr.number,10501050 id = esc(&pr.id),10511051 title = esc(&pr.title),@@ -1143,7 +1143,7 @@ fn render_ci_index(ci: &CiInfo, site_name: &str, nav: &str, description: Option<11431143 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>step</th><th>exit</th><th>duration_ms</th></tr></thead>\n<tbody>\n");11441144 for s in &run.steps {11451145 body.push_str(&format!(1146- "<tr><td><code>{}</code></td><td>{}</td><td>{}</td></tr>\n",1146+ "<tr><td data-label=\"step\"><code>{}</code></td><td data-label=\"exit\">{}</td><td data-label=\"duration_ms\">{}</td></tr>\n",11471147 esc(&s.name),11481148 s.exit,11491149 s.duration_ms,@@ -1690,7 +1690,7 @@ mod tests {16901690 html.contains("<div class=\"table-scroll\"><table>"),16911691 "{page}: wrapper"16921692 );1693- assert!(html.contains("<td class=\"title\">"), "{page}: title cell");1693+ assert!(html.contains("<td class=\"title\""), "{page}: title cell");16941694 assert!(html.contains(&long_title), "{page}: long title kept");16951695 }16961696 let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");@@ -1750,4 +1750,96 @@ mod tests {17501750 assert!(pulls.contains("1 closed"), "pulls closed total");17511751 assert!(pulls.contains(" draft"), "draft stays marked");17521752 }1753+1754+ /// m6/no-scroll (f0rge #39): stacked-card reflow on narrow, desktop untouched.1755+ /// Research: from knowledge of Gitea mobile (issue/PR lists collapse to1756+ /// title-led cards; file table stacks name + meta) — no live fetch here.1757+ #[test]1758+ fn stacked_cards_no_scroll() {1759+ let css = include_str!("../static/style.css");1760+ assert!(css.contains("@media (max-width:767px)"), "narrow query");1761+ assert!(css.contains("overflow-x:visible"), "no scroll on narrow");1762+ assert!(1763+ css.contains("content:attr(data-label)"),1764+ "label via ::before"1765+ );1766+ assert!(css.contains(".table-scroll td::before"), "label rule");1767+ assert!(1768+ css.contains(".table-scroll td.title::before"),1769+ "title leads card"1770+ );1771+ assert!(1772+ css.contains("display:block") && css.contains("display:flex"),1773+ "card layout"1774+ );1775+ assert!(css.contains("width:max-content"), "desktop width kept");1776+ assert!(1777+ css.contains(".table-scroll th,.table-scroll td") && css.contains("white-space:nowrap"),1778+ "desktop nowrap kept"1779+ );1780+ let repo = mktemp("cards");1781+ write_forge(1782+ &repo,1783+ "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",1784+ "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: Card issue\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",1785+ );1786+ write_forge(1787+ &repo,1788+ "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",1789+ "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: Card pr\nstate: open\nbase: main\nhead: feat\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n",1790+ );1791+ write_forge(1792+ &repo,1793+ "tasks/cccccccc-cccc-cccc-cccc-cccccccccccc.md",1794+ "---\nid: cccccccc-cccc-cccc-cccc-cccccccccccc\ntitle: Card task\nstatus: todo\ncreated: 2026-09-10T03:00:00Z\n---\nwork\n",1795+ );1796+ write_forge(&repo, "ci/ci.env", "runner: local\n");1797+ std::fs::create_dir_all(repo.join(".forge").join("ci/steps")).unwrap();1798+ std::fs::write(1799+ repo.join(".forge").join("ci/steps/00_test.sh"),1800+ "#!/bin/sh\n",1801+ )1802+ .unwrap();1803+ write_forge(1804+ &repo,1805+ "ci/runs/2026-09-10T00-00-00Z/summary.md",1806+ "---\nrun: 2026-09-10T00-00-00Z\nverdict: pass\ncreated: 2026-09-10T00:00:00Z\nsteps:\n - name: 00_test.sh\n exit: 0\n duration_ms: 7\n---\nok\n",1807+ );1808+ let out = mktemp("cards-out");1809+ render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");1810+ for (page, labels) in [1811+ (1812+ "issues/index.html",1813+ vec!["#", "title", "state", "comments", "created"],1814+ ),1815+ (1816+ "pulls/index.html",1817+ vec!["#", "title", "state", "head→base", "created"],1818+ ),1819+ ("ci/index.html", vec!["step", "exit", "duration_ms"]),1820+ ] {1821+ let html = std::fs::read_to_string(out.join(page)).expect(page);1822+ let tds = html.matches("<td").count();1823+ assert!(tds > 0, "{page} has cells");1824+ assert_eq!(1825+ html.matches("data-label=").count(),1826+ tds,1827+ "{page}: every td labeled"1828+ );1829+ for label in labels {1830+ assert!(1831+ html.contains(&format!("data-label=\"{label}\"")),1832+ "{page}: label {label}"1833+ );1834+ }1835+ }1836+ let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");1837+ assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");1838+ assert!(tasks.contains("todo (1)"), "tasks group kept");1839+ let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");1840+ assert!(1841+ issues.contains("open") && issues.contains("2026-09-10"),1842+ "issues data kept in DOM"1843+ );1844+ }17531845 }
src/releases.rs+44 −2--- a/src/releases.rs+++ b/src/releases.rs@@ -391,7 +391,7 @@ pub fn render_loaded(391391 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>version</th><th>title</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");392392 for rel in releases {393393 body.push_str(&format!(394- "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\">{t}</td><td>{c}</td><td>{n}</td></tr>\n",394+ "<tr><td data-label=\"version\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\" data-label=\"title\">{t}</td><td data-label=\"created\">{c}</td><td data-label=\"artifacts\">{n}</td></tr>\n",395395 v = esc(&rel.version),396396 ve = esc(&rel.version),397397 t = esc(rel.display_title()),@@ -493,7 +493,7 @@ mod tests {493493 index.contains("<div class=\"table-scroll\"><table>"),494494 "wrapper"495495 );496- assert!(index.contains("<td class=\"title\">"), "title cell");496+ assert!(index.contains("<td class=\"title\""), "title cell");497497 assert!(index.contains(&long_title), "long title kept");498498 }499499 @@ -510,4 +510,46 @@ mod tests {510510 std::fs::create_dir_all(&dir).unwrap();511511 assert!(load(&dir).is_empty());512512 }513+514+ /// m6/no-scroll (f0rge #39): release rows carry data-labels for cards.515+ #[test]516+ fn release_cells_have_data_labels() {517+ let css = include_str!("../static/style.css");518+ assert!(css.contains("content:attr(data-label)"), "label rule");519+ let rel = Release {520+ version: "v1.2.3".to_string(),521+ title: Some("Labeled".to_string()),522+ created: "2026-09-10T03:00:00Z".to_string(),523+ sha: None,524+ body: "notes\n".to_string(),525+ artifacts: Vec::new(),526+ };527+ let repo = std::env::temp_dir().join(format!(528+ "hu13-rel-labels-{}-{}",529+ std::process::id(),530+ std::time::SystemTime::now()531+ .duration_since(std::time::UNIX_EPOCH)532+ .map(|d| d.as_nanos())533+ .unwrap_or(0)534+ ));535+ std::fs::create_dir_all(&repo).unwrap();536+ let out = repo.join("out");537+ std::fs::create_dir_all(&out).unwrap();538+ render_loaded(&[rel], &repo, &out, "test", "<nav>", None).expect("render");539+ let index =540+ std::fs::read_to_string(out.join("releases").join("index.html")).expect("index");541+ let tds = index.matches("<td").count();542+ assert!(tds > 0, "has cells");543+ assert_eq!(544+ index.matches("data-label=").count(),545+ tds,546+ "every td labeled"547+ );548+ for label in ["version", "title", "created", "artifacts"] {549+ assert!(550+ index.contains(&format!("data-label=\"{label}\"")),551+ "label {label}"552+ );553+ }554+ }513555 }
static/style.css+21 −0--- 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)183183 details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan)}184184 details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px}185185 .clone-hint,.clonebox summary .muted{font-weight:400}186+/* Stacked cards on narrow (m6/no-scroll, f0rge #39): Gitea-style reflow.187+ Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.188+ Narrow: no scroll (overflow visible), thead clipped for SR only, each189+ tr becomes a bordered card, each td a labeled flex row via data-label.190+ Title cells lead bold without a repeated label; nothing is deleted. */191+@media (max-width:767px){192+ .table-scroll{overflow-x:visible}193+ .table-scroll table,.file-table table{width:100%;min-width:0}194+ .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}195+ .table-scroll tbody,.table-scroll tr{display:block;width:auto}196+ .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}197+ .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}198+ .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}199+ .table-scroll td:last-child{border-bottom:none}200+ .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}201+ .table-scroll td.title::before{display:none}202+ .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}203+ .table-scroll td.num,.table-scroll td.sha{width:auto}204+ .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}205+ .table-scroll tbody tr:hover td{background:none}206+}
tests/forge_readers.rs+5 −5--- a/tests/forge_readers.rs+++ b/tests/forge_readers.rs@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {224224 225225 // --- issues index: 2 rendered rows (broken one skipped) ---226226 let issues = read("issues/index.html");227- let rows = issues.matches("<tr><td class=\"num\">").count();227+ let rows = issues.matches("<tr><td class=\"num\"").count();228228 assert_eq!(rows, 2, "rendered issues match file-count minus broken");229229 assert!(!issues.contains("No title here"), "broken issue skipped");230230 // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.231231 assert!(232232 issues.contains(&format!(233- "<td class=\"num\">1</td><td class=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"233+ "<td class=\"num\" data-label=\"#\">1</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"234234 )),235235 "issue #1 -> older uuid"236236 );237237 assert!(238238 issues.contains(&format!(239- "<td class=\"num\">2</td><td class=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"239+ "<td class=\"num\" data-label=\"#\">2</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"240240 )),241241 "issue #2 -> newer uuid"242242 );@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {263263 // (4): pulls — same shape + head/base/draft/ci + patch <pre>.264264 let pulls = read("pulls/index.html");265265 assert_eq!(266- pulls.matches("<tr><td class=\"num\">").count(),266+ pulls.matches("<tr><td class=\"num\"").count(),267267 1,268268 "prs match file-count"269269 );270270 assert!(271271 pulls.contains(&format!(272- "<td class=\"num\">1</td><td class=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"272+ "<td class=\"num\" data-label=\"#\">1</td><td class=\"title\" data-label=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"273273 )),274274 "pr #1 -> uuid"275275 );
tests/releases_api.rs+5 −5--- a/tests/releases_api.rs+++ b/tests/releases_api.rs@@ -110,7 +110,7 @@ fn releases_artifacts_api_agents_feed() {110110 111111 // --- releases: index + both detail pages ---112112 let index = read("releases/index.html");113- assert_eq!(index.matches("<tr><td>").count(), 2, "two release rows");113+ assert_eq!(index.matches("<tr><td").count(), 2, "two release rows");114114 assert!(index.contains("v1.0"), "v1.0 listed");115115 assert!(index.contains("v2.0"), "v2.0 listed");116116 @@ -154,15 +154,15 @@ fn releases_artifacts_api_agents_feed() {154154 155155 // --- api counts == html counts (same struct pass) ---156156 let issues_html = read("issues/index.html")157- .matches("<tr><td class=\"num\">")157+ .matches("<tr><td class=\"num\"")158158 .count();159159 let pulls_html = read("pulls/index.html")160- .matches("<tr><td class=\"num\">")160+ .matches("<tr><td class=\"num\"")161161 .count();162162 let commits_html = read("commits.html")163- .matches("<tr><td class=\"sha\">")163+ .matches("<tr><td class=\"sha\"")164164 .count();165- let releases_html = read("releases/index.html").matches("<tr><td>").count();165+ let releases_html = read("releases/index.html").matches("<tr><td").count();166166 let wiki_html = read("wiki/index.html")167167 .matches("<li><a href=\"/wiki/")168168 .count();
tests/responsive.rs+4 −5--- a/tests/responsive.rs+++ b/tests/responsive.rs@@ -377,23 +377,23 @@ fn forge_list_tables_scroll_and_wrap_long_titles() {377377 for page in ["issues/index.html", "pulls/index.html"] {378378 let html = read(page);379379 assert!(380- html.contains("<td class=\"title\">"),380+ html.contains("<td class=\"title\""),381381 "{page} marks title cells"382382 );383383 assert!(384384 html.contains(&long_title),385385 "{page} keeps the full long title"386386 );387- assert!(html.contains("<td class=\"num\">"), "{page} keeps # narrow");387+ assert!(html.contains("<td class=\"num\""), "{page} keeps # narrow");388388 }389389 let commits = read("commits.html");390390 assert!(391- commits.contains("<td class=\"sha\">"),391+ commits.contains("<td class=\"sha\""),392392 "commits marks sha cells"393393 );394394 let releases = read("releases/index.html");395395 assert!(396- releases.contains("<td class=\"title\">"),396+ releases.contains("<td class=\"title\""),397397 "releases marks title cells"398398 );399399 // Labels from the fixture still list on the issues index.400--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.