#14 m6/tables

mergedpassopened
details
branches
master ← m6/tables
updated
id
a88ce902-5705-465b-9196-11f427b39a01

Patch

src/commits.rs+1 −1
diff --git a/src/commits.rs b/src/commits.rsindex a98c017..d258631 100644--- a/src/commits.rs+++ b/src/commits.rs@@ -38,7 +38,7 @@ pub fn render_loaded(3838         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");3939         for c in log {4040             body.push_str(&format!(41-                "<tr><td><a href=\"/commit/{}.html\"><code>{}</code></a></td><td>{}</td><td>{}</td><td>{}</td></tr>\n",41+                "<tr><td class=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\">{}</td><td>{}</td><td>{}</td></tr>\n",4242                 esc(&c.sha),4343                 esc(short_sha(&c.sha)),4444                 esc(&c.subject),
src/forge.rs+2 −2
diff --git a/src/forge.rs b/src/forge.rsindex 868449e..de99cc1 100644--- a/src/forge.rs+++ b/src/forge.rs@@ -940,7 +940,7 @@ fn render_issues_index(data: &ForgeData, site_name: &str, nav: &str) -> Result<S940940         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");941941         for issue in &data.issues {942942             body.push_str(&format!(943-                "<tr><td>{n}</td><td><a href=\"/issues/{id}.html\">{title}</a></td><td>{state}</td><td>{comments}</td><td>{created}</td></tr>\n",943+                "<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",944944                 n = issue.number,945945                 id = esc(&issue.id),946946                 title = esc(&issue.title),@@ -982,7 +982,7 @@ fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str) -> String {982982             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));983983             let draft = if pr.draft { " draft" } else { "" };984984             body.push_str(&format!(985-                "<tr><td>{n}</td><td><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td>{state}</td><td><code>{lanes}</code></td><td>{created}</td></tr>\n",985+                "<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",986986                 n = pr.number,987987                 id = esc(&pr.id),988988                 title = esc(&pr.title),
src/releases.rs+1 −1
diff --git a/src/releases.rs b/src/releases.rsindex e05fe4c..d0de7fd 100644--- a/src/releases.rs+++ b/src/releases.rs@@ -383,7 +383,7 @@ pub fn render_loaded(383383         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");384384         for rel in releases {385385             body.push_str(&format!(386-                "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td>{t}</td><td>{c}</td><td>{n}</td></tr>\n",386+                "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\">{t}</td><td>{c}</td><td>{n}</td></tr>\n",387387                 v = esc(&rel.version),388388                 ve = esc(&rel.version),389389                 t = esc(rel.display_title()),
static/style.css+3 −0
diff --git a/static/style.css b/static/style.cssindex 2bac0f6..32f05a8 100644--- a/static/style.css+++ b/static/style.css@@ -42,6 +42,9 @@ th,td{text-align:left;padding:.5rem .75rem;border:1px solid #292e42;vertical-ali4242 th{background:#1f2335;color:#7dcfff}4343 tr:nth-child(even) td{background:#1a1b26}4444 td code,th code{word-break:break-all}45+/* List tables (m6/tables): long titles/shas wrap inside the scroll box; the # column stays narrow. */46+td.title,td.sha{overflow-wrap:anywhere}47+td.num{width:1%;white-space:nowrap}4548 /* Single column on mobile; side-by-side only when it fits (>=768px). */4649 .cols{display:grid;grid-template-columns:minmax(0,1fr);gap:1.5rem;min-width:0}4750 .cols>section,.cols>div{min-width:0}
tests/forge_readers.rs+11 −5
diff --git a/tests/forge_readers.rs b/tests/forge_readers.rsindex f512a39..fda2b8b 100644--- 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>").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>1</td><td><a href=\"/issues/{OLDER_UUID}.html\">"233+            "<td class=\"num\">1</td><td class=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"234234         )),235235         "issue #1 -> older uuid"236236     );237237     assert!(238238         issues.contains(&format!(239-            "<td>2</td><td><a href=\"/issues/{NEWER_UUID}.html\">"239+            "<td class=\"num\">2</td><td class=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"240240         )),241241         "issue #2 -> newer uuid"242242     );@@ -262,9 +262,15 @@ fn forge_fixture_renders_counts_numbers_and_ci() {262262 263263     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.264264     let pulls = read("pulls/index.html");265-    assert_eq!(pulls.matches("<tr><td>").count(), 1, "prs match file-count");265+    assert_eq!(266+        pulls.matches("<tr><td class=\"num\">").count(),267+        1,268+        "prs match file-count"269+    );266270     assert!(267-        pulls.contains(&format!("<td>1</td><td><a href=\"/pulls/{PR_UUID}.html\">")),271+        pulls.contains(&format!(272+            "<td class=\"num\">1</td><td class=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"273+        )),268274         "pr #1 -> uuid"269275     );270276     let pr = read(&format!("pulls/{PR_UUID}.html"));
tests/releases_api.rs+9 −3
diff --git a/tests/releases_api.rs b/tests/releases_api.rsindex 57345e3..9ea9a71 100644--- a/tests/releases_api.rs+++ b/tests/releases_api.rs@@ -153,9 +153,15 @@ fn releases_artifacts_api_agents_feed() {153153     );154154 155155     // --- api counts == html counts (same struct pass) ---156-    let issues_html = read("issues/index.html").matches("<tr><td>").count();157-    let pulls_html = read("pulls/index.html").matches("<tr><td>").count();158-    let commits_html = read("commits.html").matches("<tr><td>").count();156+    let issues_html = read("issues/index.html")157+        .matches("<tr><td class=\"num\">")158+        .count();159+    let pulls_html = read("pulls/index.html")160+        .matches("<tr><td class=\"num\">")161+        .count();162+    let commits_html = read("commits.html")163+        .matches("<tr><td class=\"sha\">")164+        .count();159165     let releases_html = read("releases/index.html").matches("<tr><td>").count();160166     let wiki_html = read("wiki/index.html")161167         .matches("<li><a href=\"/wiki/")
tests/responsive.rs+164 −1
diff --git a/tests/responsive.rs b/tests/responsive.rsindex 1b21cf9..37b536e 100644--- a/tests/responsive.rs+++ b/tests/responsive.rs@@ -285,3 +285,167 @@ fn zero_js_and_reader_friendly() {285285         }286286     }287287 }288+289+fn forge_write(repo: &Path, rel: &str, content: &str) {290+    let dest = repo.join(".forge").join(rel);291+    std::fs::create_dir_all(dest.parent().expect("parent")).unwrap();292+    std::fs::write(&dest, content).unwrap();293+}294+295+/// Long-title forge fixture (m6, f0rge issue #17): one 200-char unbroken296+/// title on an issue (+ labels) and on a PR, plus one task, one CI run297+/// with steps, and one release — so every list page renders non-empty.298+/// Returns `(dist_dir, long_title)`.299+fn build_long_title_fixture() -> (PathBuf, String) {300+    let repo = fixture_repo();301+    let long_title = "L".repeat(200);302+    forge_write(303+        &repo,304+        "issues/aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa/issue.md",305+        &format!(306+            "---\nid: aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa\ntitle: {long_title}\nstate: open\nlabels:\n- kind:scroll-test\nassignees: []\ncreated: 2026-09-10T03:00:00Z\n---\nLong-title body.\n"307+        ),308+    );309+    forge_write(310+        &repo,311+        "prs/bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb/pr.md",312+        &format!(313+            "---\nid: bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb\ntitle: {long_title}\nstate: open\nbase: main\nhead: long-title-branch\ncreated: 2026-09-10T03:30:00Z\n---\nLong-title PR body.\n"314+        ),315+    );316+    forge_write(317+        &repo,318+        "tasks/cccccccc-cccc-4ccc-cccc-cccccccccccc.md",319+        "---\nid: cccccccc-cccc-4ccc-cccc-cccccccccccc\ntitle: Scroll task\nstatus: todo\ncreated: 2026-09-10T03:00:00Z\n---\nTask body.\n",320+    );321+    forge_write(&repo, "labels.md", "- name: kind:scroll-test\n");322+    forge_write(&repo, "ci/ci.env", "runner: local\n");323+    forge_write(&repo, "ci/steps/00_wrap.sh", "#!/bin/sh\nexit 0\n");324+    forge_write(325+        &repo,326+        "ci/runs/2026-09-10T00-00-00Z/summary.md",327+        "---\nrun: 2026-09-10T00-00-00Z\nverdict: pass\ncreated: 2026-09-10T00:00:00Z\nsteps:\n  - name: 00_wrap.sh\n    exit: 0\n    duration_ms: 3\n---\nwrap log\n",328+    );329+    forge_write(330+        &repo,331+        "releases/v9.9.md",332+        "---\nversion: v9.9\ntitle: Scroll release\ncreated: 2026-09-10T02:00:00Z\n---\nRelease notes.\n",333+    );334+    git(&repo, &["add", "-A"]);335+    git(&repo, &["commit", "-m", "long-title forge fixture"]);336+    let out = fresh("tables");337+    let st = Command::new(bin())338+        .arg("build")339+        .arg(&repo)340+        .arg("-o")341+        .arg(&out)342+        .status()343+        .expect("spawn hu13 build");344+    assert!(st.success(), "long-title fixture build exits 0");345+    (out, long_title)346+}347+348+#[test]349+fn forge_list_tables_scroll_and_wrap_long_titles() {350+    let (out, long_title) = build_long_title_fixture();351+    let read = |rel: &str| std::fs::read_to_string(out.join(rel)).expect(rel);352+    // Every table-bearing list page wraps its table in .table-scroll and353+    // leaves no bare <table> outside the scroll container. The wrapper is354+    // the 360px story: the table scrolls inside instead of forcing the page.355+    for page in [356+        "issues/index.html",357+        "pulls/index.html",358+        "commits.html",359+        "releases/index.html",360+        "ci/index.html",361+    ] {362+        let html = read(page);363+        assert!(html.contains("table-scroll"), "{page} wraps its table");364+        assert!(html.contains("<table"), "{page} keeps <table>");365+        let stripped = html.replace("<div class=\"table-scroll\"><table>", "");366+        assert!(367+            !stripped.contains("<table"),368+            "{page} has no bare <table> outside .table-scroll"369+        );370+    }371+    // Tasks list renders grouped <ul>s, never a table.372+    let tasks = read("tasks/index.html");373+    assert!(!tasks.contains("<table"), "tasks page has no <table>");374+    // 200-char titles render in full (wrapped, not cut) inside title cells:375+    // the cell class is the no-page-widen hook (overflow-wrap:anywhere),376+    // and the # column keeps its narrow marker class.377+    for page in ["issues/index.html", "pulls/index.html"] {378+        let html = read(page);379+        assert!(380+            html.contains("<td class=\"title\">"),381+            "{page} marks title cells"382+        );383+        assert!(384+            html.contains(&long_title),385+            "{page} keeps the full long title"386+        );387+        assert!(html.contains("<td class=\"num\">"), "{page} keeps # narrow");388+    }389+    let commits = read("commits.html");390+    assert!(391+        commits.contains("<td class=\"sha\">"),392+        "commits marks sha cells"393+    );394+    let releases = read("releases/index.html");395+    assert!(396+        releases.contains("<td class=\"title\">"),397+        "releases marks title cells"398+    );399+    // Labels from the fixture still list on the issues index.400+    assert!(401+        read("issues/index.html").contains("kind:scroll-test"),402+        "labels listed"403+    );404+    // CSS side: title/sha cells wrap anywhere, # column stays narrow.405+    let css = css();406+    for needle in [407+        "td.title",408+        "td.sha",409+        "td.num",410+        "overflow-wrap:anywhere",411+        "white-space:nowrap",412+    ] {413+        assert!(css.contains(needle), "CSS mentions {needle}");414+    }415+}416+417+#[test]418+fn zero_bare_tables_in_sources() {419+    // Audit EVERY template (m6, f0rge issue #17): no `<table>` may open420+    // outside a `.table-scroll` container. All tables are emitted from421+    // src/*.rs render fns (markdown never emits tables), so grep the422+    // sources: each line opening a table must carry the scroll wrapper.423+    let mut checked = 0;424+    let mut stack = vec![manifest().join("src")];425+    while let Some(d) = stack.pop() {426+        for e in std::fs::read_dir(&d).expect("read src") {427+            let e = e.expect("entry");428+            let p = e.path();429+            if p.is_dir() {430+                stack.push(p);431+                continue;432+            }433+            if p.extension().is_none_or(|x| x != "rs") {434+                continue;435+            }436+            let text = std::fs::read_to_string(&p).expect("read source");437+            for (i, line) in text.lines().enumerate() {438+                if line.contains("<table") {439+                    checked += 1;440+                    assert!(441+                        line.contains("table-scroll"),442+                        "{}:{} opens a bare <table>",443+                        p.display(),444+                        i + 1445+                    );446+                }447+            }448+        }449+    }450+    assert!(checked >= 5, "audit saw all list tables, got {checked}");451+}288-- 2.55.0

Comments (0)

No comments yet.

Reviews (0)

No reviews yet.

all pulls