.forge/prs/019a1079-9f9b-46d9-bbcc-9ffc0d4dffb2/patch.diff 10.3 KiB 238 lines Raw
1 From 4eabe68a356be4631d544340616cca4166ffa0a6 Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 07:34:03 +01004 Subject: [PATCH] m6/table2: natural scroll tables + inline title buttons5 6 ---7  src/commits.rs   | 30 ++++++++++++++++8  src/forge.rs     | 93 +++++++++++++++++++++++++++++++++++++++++++++---9  src/releases.rs  | 35 ++++++++++++++++++10  static/style.css | 12 +++++++11  4 files changed, 166 insertions(+), 4 deletions(-)12 13 diff --git a/src/commits.rs b/src/commits.rs14 index 9c3b288..d5fa33e 10064415 --- a/src/commits.rs16 +++ b/src/commits.rs17 @@ -322,6 +322,36 @@ mod tests {18          assert!(!out.join("commit").join(format!("{}.html", all[2])).exists());19      }20  21 +    /// m6/table-scroll (f0rge #30): long subject + full sha render inside a22 +    /// .table-scroll wrapper with td.title / td.sha cells.23 +    #[test]24 +    fn long_subject_scrolls_with_title_cell() {25 +        let dir = mktemp("long-subject");26 +        git(&dir, &["init", "-b", "main"]);27 +        git(&dir, &["config", "user.name", "hu13-test"]);28 +        git(&dir, &["config", "user.email", "[email protected]"]);29 +        git(&dir, &["config", "commit.gpgsign", "false"]);30 +        let long_subject = "s".repeat(200);31 +        std::fs::write(dir.join("x.txt"), "x\n").unwrap();32 +        git(&dir, &["add", "-A"]);33 +        git(&dir, &["commit", "-m", &long_subject]);34 +        let out = mktemp("out");35 +        render(&dir, &out, "main", 100, "test", "<nav>", None).expect("render");36 +        let list = std::fs::read_to_string(out.join("commits.html")).expect("list");37 +        assert!(38 +            list.contains("<div class=\"table-scroll\"><table>"),39 +            "wrapper"40 +        );41 +        assert!(list.contains("<td class=\"title\">"), "title cell");42 +        assert!(list.contains("<td class=\"sha\">"), "sha cell");43 +        assert!(list.contains(&long_subject), "long subject kept");44 +        let sha = shas(&dir)[0].clone();45 +        assert!(46 +            list.contains(&format!("/commit/{sha}.html")),47 +            "full sha link"48 +        );49 +    }50 +51      #[test]52      fn empty_log_renders_none_yet() {53          let dir = mktemp("empty");54 diff --git a/src/forge.rs b/src/forge.rs55 index 1f8af3e..3574c8a 10064456 --- a/src/forge.rs57 +++ b/src/forge.rs58 @@ -947,8 +947,8 @@ fn render_issues_index(59      nav: &str,60      description: Option<&str>,61  ) -> Result<String, String> {62 -    let mut body = String::from("<h1>Issues</h1>\n");63 -    body.push_str("<p><a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></p>\n");64 +    let mut body =65 +        String::from("<div class=\"page-head\"><h1>Issues</h1>\n<a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></div>\n");66      if data.issues.is_empty() {67          body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");68      } else {69 @@ -994,8 +994,8 @@ fn render_issues_index(70  }71  72  fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str, description: Option<&str>) -> String {73 -    let mut body = String::from("<h1>Pulls</h1>\n");74 -    body.push_str("<p><a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></p>\n");75 +    let mut body =76 +        String::from("<div class=\"page-head\"><h1>Pulls</h1>\n<a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></div>\n");77      if prs.is_empty() {78          body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");79      } else {80 @@ -1566,4 +1566,89 @@ mod tests {81          assert!(!data.ci.steps.is_empty(), "real ci steps read");82          assert!(data.ci.latest.is_some(), "real ci latest run read");83      }84 +85 +    /// m6/table-scroll (f0rge #30): CSS keeps natural width + nowrap cells86 +    /// with a wrapping title floor; hover tint via var.87 +    #[test]88 +    fn table_scroll_css_rules() {89 +        let css = include_str!("../static/style.css");90 +        assert!(css.contains("width:max-content"), "natural width");91 +        assert!(css.contains("min-width:100%"), "full-bleed floor");92 +        assert!(93 +            css.contains(".table-scroll th,.table-scroll td") && css.contains("white-space:nowrap"),94 +            "cells nowrap"95 +        );96 +        assert!(97 +            css.contains(".table-scroll td.title") && css.contains("min-width:12rem"),98 +            "title floor"99 +        );100 +        assert!(101 +            css.contains("tr:hover td") && css.contains("var(--bg-th)"),102 +            "hover tint"103 +        );104 +    }105 +106 +    /// m6/table-scroll (f0rge #31): h1 + button share one .page-head row.107 +    #[test]108 +    fn page_head_css_and_markup() {109 +        let css = include_str!("../static/style.css");110 +        assert!(css.contains(".page-head"), "page-head rule");111 +        assert!(css.contains("align-items:center"), "inline row");112 +        let repo = mktemp("page-head");113 +        let out = mktemp("page-head-out");114 +        render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");115 +        for (page, title, btn) in [116 +            ("issues/index.html", "<h1>Issues</h1>", "+ New issue"),117 +            ("pulls/index.html", "<h1>Pulls</h1>", "+ New PR prompt"),118 +        ] {119 +            let html = std::fs::read_to_string(out.join(page)).expect(page);120 +            let head = html.find("<div class=\"page-head\">").expect("page-head");121 +            let h1 = html.find(title).expect("h1");122 +            let b = html.find(btn).expect("button");123 +            let close = html[head..].find("</div>").expect("close") + head;124 +            assert!(125 +                h1 > head && b > head && h1 < close && b < close,126 +                "{page}: h1+btn in page-head"127 +            );128 +        }129 +        // Tasks has no submit button (no page-head needed).130 +        let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");131 +        assert!(!tasks.contains("btn-new"), "tasks: no submit button");132 +    }133 +134 +    /// m6/table-scroll (f0rge #30): 200-char title + long sha stay in a135 +    /// .table-scroll wrapper with a td.title cell (scroll, never squash).136 +    #[test]137 +    fn long_title_and_sha_scroll() {138 +        let long_title = "t".repeat(200);139 +        let long_sha = "a".repeat(64);140 +        let repo = mktemp("long-title");141 +        write_forge(142 +            &repo,143 +            "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",144 +            &format!(145 +                "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: {long_title}\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n"146 +            ),147 +        );148 +        write_forge(149 +            &repo,150 +            "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",151 +            &format!(152 +                "---\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"153 +            ),154 +        );155 +        let out = mktemp("long-title-out");156 +        render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");157 +        for page in ["issues/index.html", "pulls/index.html"] {158 +            let html = std::fs::read_to_string(out.join(page)).expect(page);159 +            assert!(160 +                html.contains("<div class=\"table-scroll\"><table>"),161 +                "{page}: wrapper"162 +            );163 +            assert!(html.contains("<td class=\"title\">"), "{page}: title cell");164 +            assert!(html.contains(&long_title), "{page}: long title kept");165 +        }166 +        let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");167 +        assert!(pulls.contains(&long_sha), "long head sha kept");168 +    }169  }170 diff --git a/src/releases.rs b/src/releases.rs171 index 10d39c7..ad238d9 100644172 --- a/src/releases.rs173 +++ b/src/releases.rs174 @@ -462,6 +462,41 @@ mod tests {175          assert_eq!(r.display_title(), "v1.0");176      }177  178 +    /// m6/table-scroll (f0rge #30): 200-char release title renders in a179 +    /// .table-scroll wrapper with a td.title cell.180 +    #[test]181 +    fn long_release_title_scrolls_with_title_cell() {182 +        let long_title = "r".repeat(200);183 +        let rel = Release {184 +            version: "v9.9.9".to_string(),185 +            title: Some(long_title.clone()),186 +            created: "2026-09-10T03:00:00Z".to_string(),187 +            sha: Some("a".repeat(64)),188 +            body: "notes\n".to_string(),189 +            artifacts: Vec::new(),190 +        };191 +        let repo = std::env::temp_dir().join(format!(192 +            "hu13-rel-long-{}-{}",193 +            std::process::id(),194 +            std::time::SystemTime::now()195 +                .duration_since(std::time::UNIX_EPOCH)196 +                .map(|d| d.as_nanos())197 +                .unwrap_or(0)198 +        ));199 +        std::fs::create_dir_all(&repo).unwrap();200 +        let out = repo.join("out");201 +        std::fs::create_dir_all(&out).unwrap();202 +        render_loaded(&[rel], &repo, &out, "test", "<nav>", None).expect("render");203 +        let index =204 +            std::fs::read_to_string(out.join("releases").join("index.html")).expect("index");205 +        assert!(206 +            index.contains("<div class=\"table-scroll\"><table>"),207 +            "wrapper"208 +        );209 +        assert!(index.contains("<td class=\"title\">"), "title cell");210 +        assert!(index.contains(&long_title), "long title kept");211 +    }212 +213      #[test]214      fn missing_dir_is_empty() {215          let dir = std::env::temp_dir().join(format!(216 diff --git a/static/style.css b/static/style.css217 index ad2b9da..5234011 100644218 --- a/static/style.css219 +++ b/static/style.css220 @@ -157,3 +157,15 @@ a.ln:hover,a.ln:focus{text-decoration:underline}221  .clone-block h3{margin:1rem 0 .25rem}222  .clone-block p{margin:.5rem 0}223  .clone-cmd code{user-select:all;overflow-wrap:anywhere;word-break:break-all}224 +/* List tables scroll, never squash (m6/table-scroll, f0rge #30): natural225 +   width inside .table-scroll; cells nowrap except title cells which wrap226 +   with a floor so rows scroll horizontally. Hover tint via var. */227 +.table-scroll table{width:max-content;min-width:100%}228 +.table-scroll th,.table-scroll td{white-space:nowrap}229 +.table-scroll td.title{white-space:normal;overflow-wrap:anywhere;min-width:12rem}230 +.table-scroll tbody tr:hover td{background:var(--bg-th)}231 +/* Page titles with inline buttons (m6/table-scroll, f0rge #31): h1 +232 +   button share one flex row; wrap allowed on 360px but stays adjacent. */233 +.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem}234 +.page-head h1{margin:0}235 +.page-head .btn-new{flex:none}236 -- 237 2.55.0238