#27 m6/table2
Patch
src/commits.rs+30 −0--- a/src/commits.rs+++ b/src/commits.rs@@ -322,6 +322,36 @@ mod tests {322322 assert!(!out.join("commit").join(format!("{}.html", all[2])).exists());323323 }324324 325+ /// m6/table-scroll (f0rge #30): long subject + full sha render inside a326+ /// .table-scroll wrapper with td.title / td.sha cells.327+ #[test]328+ fn long_subject_scrolls_with_title_cell() {329+ let dir = mktemp("long-subject");330+ git(&dir, &["init", "-b", "main"]);331+ git(&dir, &["config", "user.name", "hu13-test"]);332+ git(&dir, &["config", "user.email", "[email protected]"]);333+ git(&dir, &["config", "commit.gpgsign", "false"]);334+ let long_subject = "s".repeat(200);335+ std::fs::write(dir.join("x.txt"), "x\n").unwrap();336+ git(&dir, &["add", "-A"]);337+ git(&dir, &["commit", "-m", &long_subject]);338+ let out = mktemp("out");339+ render(&dir, &out, "main", 100, "test", "<nav>", None).expect("render");340+ let list = std::fs::read_to_string(out.join("commits.html")).expect("list");341+ assert!(342+ list.contains("<div class=\"table-scroll\"><table>"),343+ "wrapper"344+ );345+ assert!(list.contains("<td class=\"title\">"), "title cell");346+ assert!(list.contains("<td class=\"sha\">"), "sha cell");347+ assert!(list.contains(&long_subject), "long subject kept");348+ let sha = shas(&dir)[0].clone();349+ assert!(350+ list.contains(&format!("/commit/{sha}.html")),351+ "full sha link"352+ );353+ }354+325355 #[test]326356 fn empty_log_renders_none_yet() {327357 let dir = mktemp("empty");
src/forge.rs+89 −4--- a/src/forge.rs+++ b/src/forge.rs@@ -947,8 +947,8 @@ fn render_issues_index(947947 nav: &str,948948 description: Option<&str>,949949 ) -> Result<String, String> {950- let mut body = String::from("<h1>Issues</h1>\n");951- body.push_str("<p><a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></p>\n");950+ let mut body =951+ String::from("<div class=\"page-head\"><h1>Issues</h1>\n<a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></div>\n");952952 if data.issues.is_empty() {953953 body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");954954 } else {@@ -994,8 +994,8 @@ fn render_issues_index(994994 }995995 996996 fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str, description: Option<&str>) -> String {997- let mut body = String::from("<h1>Pulls</h1>\n");998- body.push_str("<p><a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></p>\n");997+ let mut body =998+ String::from("<div class=\"page-head\"><h1>Pulls</h1>\n<a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></div>\n");999999 if prs.is_empty() {10001000 body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");10011001 } else {@@ -1566,4 +1566,89 @@ mod tests {15661566 assert!(!data.ci.steps.is_empty(), "real ci steps read");15671567 assert!(data.ci.latest.is_some(), "real ci latest run read");15681568 }1569+1570+ /// m6/table-scroll (f0rge #30): CSS keeps natural width + nowrap cells1571+ /// with a wrapping title floor; hover tint via var.1572+ #[test]1573+ fn table_scroll_css_rules() {1574+ let css = include_str!("../static/style.css");1575+ assert!(css.contains("width:max-content"), "natural width");1576+ assert!(css.contains("min-width:100%"), "full-bleed floor");1577+ assert!(1578+ css.contains(".table-scroll th,.table-scroll td") && css.contains("white-space:nowrap"),1579+ "cells nowrap"1580+ );1581+ assert!(1582+ css.contains(".table-scroll td.title") && css.contains("min-width:12rem"),1583+ "title floor"1584+ );1585+ assert!(1586+ css.contains("tr:hover td") && css.contains("var(--bg-th)"),1587+ "hover tint"1588+ );1589+ }1590+1591+ /// m6/table-scroll (f0rge #31): h1 + button share one .page-head row.1592+ #[test]1593+ fn page_head_css_and_markup() {1594+ let css = include_str!("../static/style.css");1595+ assert!(css.contains(".page-head"), "page-head rule");1596+ assert!(css.contains("align-items:center"), "inline row");1597+ let repo = mktemp("page-head");1598+ let out = mktemp("page-head-out");1599+ render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");1600+ for (page, title, btn) in [1601+ ("issues/index.html", "<h1>Issues</h1>", "+ New issue"),1602+ ("pulls/index.html", "<h1>Pulls</h1>", "+ New PR prompt"),1603+ ] {1604+ let html = std::fs::read_to_string(out.join(page)).expect(page);1605+ let head = html.find("<div class=\"page-head\">").expect("page-head");1606+ let h1 = html.find(title).expect("h1");1607+ let b = html.find(btn).expect("button");1608+ let close = html[head..].find("</div>").expect("close") + head;1609+ assert!(1610+ h1 > head && b > head && h1 < close && b < close,1611+ "{page}: h1+btn in page-head"1612+ );1613+ }1614+ // Tasks has no submit button (no page-head needed).1615+ let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");1616+ assert!(!tasks.contains("btn-new"), "tasks: no submit button");1617+ }1618+1619+ /// m6/table-scroll (f0rge #30): 200-char title + long sha stay in a1620+ /// .table-scroll wrapper with a td.title cell (scroll, never squash).1621+ #[test]1622+ fn long_title_and_sha_scroll() {1623+ let long_title = "t".repeat(200);1624+ let long_sha = "a".repeat(64);1625+ let repo = mktemp("long-title");1626+ write_forge(1627+ &repo,1628+ "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",1629+ &format!(1630+ "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: {long_title}\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n"1631+ ),1632+ );1633+ write_forge(1634+ &repo,1635+ "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",1636+ &format!(1637+ "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: {long_title}\nstate: open\nbase: main\nhead: {long_sha}\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n"1638+ ),1639+ );1640+ let out = mktemp("long-title-out");1641+ render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");1642+ for page in ["issues/index.html", "pulls/index.html"] {1643+ let html = std::fs::read_to_string(out.join(page)).expect(page);1644+ assert!(1645+ html.contains("<div class=\"table-scroll\"><table>"),1646+ "{page}: wrapper"1647+ );1648+ assert!(html.contains("<td class=\"title\">"), "{page}: title cell");1649+ assert!(html.contains(&long_title), "{page}: long title kept");1650+ }1651+ let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");1652+ assert!(pulls.contains(&long_sha), "long head sha kept");1653+ }15691654 }
src/releases.rs+35 −0--- a/src/releases.rs+++ b/src/releases.rs@@ -462,6 +462,41 @@ mod tests {462462 assert_eq!(r.display_title(), "v1.0");463463 }464464 465+ /// m6/table-scroll (f0rge #30): 200-char release title renders in a466+ /// .table-scroll wrapper with a td.title cell.467+ #[test]468+ fn long_release_title_scrolls_with_title_cell() {469+ let long_title = "r".repeat(200);470+ let rel = Release {471+ version: "v9.9.9".to_string(),472+ title: Some(long_title.clone()),473+ created: "2026-09-10T03:00:00Z".to_string(),474+ sha: Some("a".repeat(64)),475+ body: "notes\n".to_string(),476+ artifacts: Vec::new(),477+ };478+ let repo = std::env::temp_dir().join(format!(479+ "hu13-rel-long-{}-{}",480+ std::process::id(),481+ std::time::SystemTime::now()482+ .duration_since(std::time::UNIX_EPOCH)483+ .map(|d| d.as_nanos())484+ .unwrap_or(0)485+ ));486+ std::fs::create_dir_all(&repo).unwrap();487+ let out = repo.join("out");488+ std::fs::create_dir_all(&out).unwrap();489+ render_loaded(&[rel], &repo, &out, "test", "<nav>", None).expect("render");490+ let index =491+ std::fs::read_to_string(out.join("releases").join("index.html")).expect("index");492+ assert!(493+ index.contains("<div class=\"table-scroll\"><table>"),494+ "wrapper"495+ );496+ assert!(index.contains("<td class=\"title\">"), "title cell");497+ assert!(index.contains(&long_title), "long title kept");498+ }499+465500 #[test]466501 fn missing_dir_is_empty() {467502 let dir = std::env::temp_dir().join(format!(
static/style.css+12 −1--- a/static/style.css+++ b/static/style.css@@ -157,3 +157,15 @@ a.ln:hover,a.ln:focus{text-decoration:underline}157157 .clone-block h3{margin:1rem 0 .25rem}158158 .clone-block p{margin:.5rem 0}159159 .clone-cmd code{user-select:all;overflow-wrap:anywhere;word-break:break-all}160+/* List tables scroll, never squash (m6/table-scroll, f0rge #30): natural161+ width inside .table-scroll; cells nowrap except title cells which wrap162+ with a floor so rows scroll horizontally. Hover tint via var. */163+.table-scroll table{width:max-content;min-width:100%}164+.table-scroll th,.table-scroll td{white-space:nowrap}165+.table-scroll td.title{white-space:normal;overflow-wrap:anywhere;min-width:12rem}166+.table-scroll tbody tr:hover td{background:var(--bg-th)}167+/* Page titles with inline buttons (m6/table-scroll, f0rge #31): h1 +168+ button share one flex row; wrap allowed on 360px but stays adjacent. */169+.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem}170+.page-head h1{margin:0}171+.page-head .btn-new{flex:none}160--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.