#14 m6/tables
Patch
src/commits.rs+1 −1--- 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--- 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--- 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--- 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--- 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--- 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--- 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--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.