#43 m6/cards2-linear
Patch
.forge/merges/2026-09-10T08-57-21.752386694Z.md+59 −0--- /dev/null+++ b/.forge/merges/2026-09-10T08-57-21.752386694Z.md@@ -0,0 +1,59 @@1+---2+pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T08:57:21.752386694Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+ if data.issues.is_empty() {23+ body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+ } else {25+- 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");26++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+ for issue in &data.issues {28+ body.push_str(&format!(29+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+ n = issue.number,32+ id = esc(&issue.id),33+ title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+ if prs.is_empty() {36+ body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+ } else {38+- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+ for pr in prs {41+ let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+ let lanes_plain = format!("{}→{}", pr.head, pr.base);43+ let draft = if pr.draft { " draft" } else { "" };44+ let full_title = format!("{}{}", pr.title, draft);45+ body.push_str(&format!(46+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+ n = pr.number,49+ id = esc(&pr.id),50+ title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+ css.contains("content:attr(data-label)"),53+ "label via ::before"54+ );55+- assert!(css.contains(".table-scroll td::before"), "label rule");56+ assert!(57+- css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/merges/2026-09-10T08-58-33.103715749Z.md+59 −0--- /dev/null+++ b/.forge/merges/2026-09-10T08-58-33.103715749Z.md@@ -0,0 +1,59 @@1+---2+pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T08:58:33.103715749Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+ if data.issues.is_empty() {23+ body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+ } else {25+- 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");26++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+ for issue in &data.issues {28+ body.push_str(&format!(29+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+ n = issue.number,32+ id = esc(&issue.id),33+ title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+ if prs.is_empty() {36+ body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+ } else {38+- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+ for pr in prs {41+ let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+ let lanes_plain = format!("{}→{}", pr.head, pr.base);43+ let draft = if pr.draft { " draft" } else { "" };44+ let full_title = format!("{}{}", pr.title, draft);45+ body.push_str(&format!(46+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+ n = pr.number,49+ id = esc(&pr.id),50+ title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+ css.contains("content:attr(data-label)"),53+ "label via ::before"54+ );55+- assert!(css.contains(".table-scroll td::before"), "label rule");56+ assert!(57+- css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/merges/2026-09-10T09-01-18.492745439Z.md+59 −0--- /dev/null+++ b/.forge/merges/2026-09-10T09-01-18.492745439Z.md@@ -0,0 +1,59 @@1+---2+pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T09:01:18.492745439Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+ if data.issues.is_empty() {23+ body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+ } else {25+- 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");26++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+ for issue in &data.issues {28+ body.push_str(&format!(29+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+ n = issue.number,32+ id = esc(&issue.id),33+ title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+ if prs.is_empty() {36+ body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+ } else {38+- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+ for pr in prs {41+ let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+ let lanes_plain = format!("{}→{}", pr.head, pr.base);43+ let draft = if pr.draft { " draft" } else { "" };44+ let full_title = format!("{}{}", pr.title, draft);45+ body.push_str(&format!(46+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+ n = pr.number,49+ id = esc(&pr.id),50+ title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+ css.contains("content:attr(data-label)"),53+ "label via ::before"54+ );55+- assert!(css.contains(".table-scroll td::before"), "label rule");56+ assert!(57+- css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/merges/2026-09-10T09-01-43.282852796Z.md+59 −0--- /dev/null+++ b/.forge/merges/2026-09-10T09-01-43.282852796Z.md@@ -0,0 +1,59 @@1+---2+pr: 1329e344-0a57-4508-aecc-aca2914005be3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T09:01:43.282852796Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+ if data.issues.is_empty() {23+ body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+ } else {25+- 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");26++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+ for issue in &data.issues {28+ body.push_str(&format!(29+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+ n = issue.number,32+ id = esc(&issue.id),33+ title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+ if prs.is_empty() {36+ body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+ } else {38+- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+ for pr in prs {41+ let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+ let lanes_plain = format!("{}→{}", pr.head, pr.base);43+ let draft = if pr.draft { " draft" } else { "" };44+ let full_title = format!("{}{}", pr.title, draft);45+ body.push_str(&format!(46+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+ n = pr.number,49+ id = esc(&pr.id),50+ title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+ css.contains("content:attr(data-label)"),53+ "label via ::before"54+ );55+- assert!(css.contains(".table-scroll td::before"), "label rule");56+ assert!(57+- css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff+421 −0--- /dev/null+++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff@@ -0,0 +1,421 @@1+From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 20012+From: gl0w <gl0w@world>3+Date: Thu, 10 Sep 2026 09:57:21 +01004+Subject: [PATCH] m6/cards2: file-table exempt, title-first cards5+6+---7+ src/build.rs | 57 ++++++++++++++++++++++++++----------------8+ src/commits.rs | 12 ++++++---9+ src/forge.rs | 47 +++++++++++++++++++++++++++++-----10+ src/releases.rs | 8 ++++--11+ static/style.css | 37 +++++++++++++++------------12+ tests/forge_readers.rs | 10 ++++----13+ tests/releases_api.rs | 8 +++---14+ tests/truncate.rs | 18 +++++++++++--15+ 8 files changed, 137 insertions(+), 60 deletions(-)16+17+diff --git a/src/build.rs b/src/build.rs18+index 6a12e44..f1c0795 10064419+--- a/src/build.rs20++++ b/src/build.rs21+@@ -579,13 +579,13 @@ pub fn file_table_html(22+ history: &std::collections::BTreeMap<String, (String, String)>,23+ ) -> String {24+ let mut out = String::from(25+- "<div class=\"table-scroll\"><table class=\"file-table\">\n\26++ "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\27+ <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",28+ );29+ if let Some((href, label)) = parent {30+ out.push_str(&format!(31+- "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\32+- <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",33++ "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\34++ <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",35+ esc(label),36+ esc(href),37+ esc(label)38+@@ -601,8 +601,8 @@ pub fn file_table_html(39+ let msg_short = truncate_msg(&msg);40+ if e.is_dir {41+ out.push_str(&format!(42+- "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\43+- <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",44++ "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\45++ <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",46+ esc(&e.name),47+ href_esc(&full),48+ esc(&e.name),49+@@ -613,8 +613,8 @@ pub fn file_table_html(50+ ));51+ } else {52+ out.push_str(&format!(53+- "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\54+- <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",55++ "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\56++ <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",57+ esc(&e.name),58+ href_esc(&full),59+ esc(&e.name),60+@@ -1732,28 +1732,41 @@ mod tests {61+ assert!(sub.contains("title=\""), "title attrs kept");62+ }63+ 64+- /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.65++ /// m6/cards2 (f0rge #46): file table stays a real table on all widths —66++ /// no data-label card treatment, scroll wrapper kept, trunc + title kept.67+ #[test]68+- fn file_table_cells_have_data_labels() {69++ fn file_table_stays_table_no_card_labels() {70+ let css = include_str!("../static/style.css");71+- assert!(css.contains("content:attr(data-label)"), "label rule");72+ assert!(css.contains("@media (max-width:767px)"), "narrow query");73++ assert!(74++ css.contains(".table-scroll:not(.file-scroll)"),75++ "cards scoped off file-scroll"76++ );77++ assert!(78++ css.contains(".file-scroll{overflow-x:auto}"),79++ "file-scroll keeps h-scroll on narrow"80++ );81+ let (_repo, out) = build_fixture();82+ for page in ["index.html", "files/sub.html"] {83+ let html = std::fs::read_to_string(out.join(page)).expect(page);84+- let tds = html.matches("<td").count();85+- assert!(tds > 0, "{page} has cells");86+- assert_eq!(87+- html.matches("data-label=").count(),88+- tds,89+- "{page}: every td labeled"90++ assert!(91++ html.contains(92++ "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"93++ ),94++ "{page}: scroll wrapper kept"95++ );96++ assert!(!html.contains("data-label="), "{page}: no card labels");97++ assert!(98++ html.contains("<td class=\"title trunc trunc-message\""),99++ "{page}: message cell trunc kept"100++ );101++ assert!(html.contains("title=\""), "{page}: title attrs kept");102++ assert!(103++ html.contains("<th>Name</th>")104++ && html.contains("<th>Last commit</th>")105++ && html.contains("<th>Age</th>"),106++ "{page}: headers kept"107+ );108+- for label in ["Name", "Last commit", "Age"] {109+- assert!(110+- html.contains(&format!("data-label=\"{label}\"")),111+- "{page}: label {label}"112+- );113+- }114+ }115+ }116+ 117+diff --git a/src/commits.rs b/src/commits.rs118+index 2324ee1..e3aa5de 100644119+--- a/src/commits.rs120++++ b/src/commits.rs121+@@ -40,15 +40,15 @@ pub fn render_loaded(122+ if log.is_empty() {123+ body.push_str("<p class=\"muted\">No commits yet.</p>\n");124+ } else {125+- 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");126++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>subject</th><th>sha</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");127+ for c in log {128+ body.push_str(&format!(129+- "<tr><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",130++ "<tr><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",131++ esc(&c.subject),132++ esc(&c.subject),133+ esc(&c.sha),134+ esc(&c.sha),135+ esc(short_sha(&c.sha)),136+- esc(&c.subject),137+- esc(&c.subject),138+ esc(&c.author),139+ esc(&c.author),140+ esc(&c.date),141+@@ -423,5 +423,9 @@ mod tests {142+ "label {label}"143+ );144+ }145++ // m6/cards2 (f0rge #47): card DOM order is subject-first.146++ let subj = list.find("data-label=\"subject\"").expect("subject cell");147++ let sha = list.find("data-label=\"sha\"").expect("sha cell");148++ assert!(subj < sha, "card DOM: subject precedes sha");149+ }150+ }151+diff --git a/src/forge.rs b/src/forge.rs152+index 40aa052..d2827b6 100644153+--- a/src/forge.rs154++++ b/src/forge.rs155+@@ -998,10 +998,10 @@ fn render_issues_index(156+ if data.issues.is_empty() {157+ body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");158+ } else {159+- 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");160++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");161+ for issue in &data.issues {162+ body.push_str(&format!(163+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",164++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",165+ n = issue.number,166+ id = esc(&issue.id),167+ title = esc(&issue.title),168+@@ -1059,14 +1059,14 @@ fn render_pulls_index(169+ if prs.is_empty() {170+ body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");171+ } else {172+- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");173++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");174+ for pr in prs {175+ let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));176+ let lanes_plain = format!("{}→{}", pr.head, pr.base);177+ let draft = if pr.draft { " draft" } else { "" };178+ let full_title = format!("{}{}", pr.title, draft);179+ body.push_str(&format!(180+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",181++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",182+ n = pr.number,183+ id = esc(&pr.id),184+ title = esc(&pr.title),185+@@ -1823,11 +1823,19 @@ mod tests {186+ css.contains("content:attr(data-label)"),187+ "label via ::before"188+ );189+- assert!(css.contains(".table-scroll td::before"), "label rule");190+ assert!(191+- css.contains(".table-scroll td.title::before"),192++ css.contains(".table-scroll:not(.file-scroll) td::before"),193++ "label rule"194++ );195++ assert!(196++ css.contains(".table-scroll:not(.file-scroll) td.title::before"),197+ "title leads card"198+ );199++ // m6/cards2 (f0rge #46): file table exempt from the card reflow.200++ assert!(201++ css.contains(".file-scroll{overflow-x:auto}"),202++ "file table keeps h-scroll on narrow"203++ );204+ assert!(205+ css.contains("display:block") && css.contains("display:flex"),206+ "card layout"207+@@ -1894,8 +1902,35 @@ mod tests {208+ );209+ }210+ }211++ // m6/cards2 (f0rge #47): card DOM order is title-first via template212++ // cell order (no CSS order hacks) — desktop column headers match.213++ for (page, first, second) in [214++ (215++ "issues/index.html",216++ "data-label=\"title\"",217++ "data-label=\"#\"",218++ ),219++ (220++ "pulls/index.html",221++ "data-label=\"title\"",222++ "data-label=\"#\"",223++ ),224++ (225++ "ci/index.html",226++ "data-label=\"step\"",227++ "data-label=\"exit\"",228++ ),229++ ] {230++ let html = std::fs::read_to_string(out.join(page)).expect(page);231++ let a = html.find(first).expect("first cell");232++ let b = html.find(second).expect("second cell");233++ assert!(a < b, "{page}: card DOM title precedes meta");234++ }235+ let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");236+ assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");237++ let title_pos = tasks.find("Card task").expect("task title");238++ let meta_pos = tasks.find("<code>cccccccc").expect("task meta");239++ assert!(title_pos < meta_pos, "tasks: title precedes meta");240+ assert!(tasks.contains("todo (1)"), "tasks group kept");241+ let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");242+ assert!(243+diff --git a/src/releases.rs b/src/releases.rs244+index 68c050b..cceb905 100644245+--- a/src/releases.rs246++++ b/src/releases.rs247+@@ -395,10 +395,10 @@ pub fn render_loaded(248+ if releases.is_empty() {249+ body.push_str("<p class=\"muted\">No releases yet.</p>\n");250+ } else {251+- 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");252++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>version</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");253+ for rel in releases {254+ body.push_str(&format!(255+- "<tr><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",256++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",257+ v = esc(&rel.version),258+ ve = esc(&rel.version),259+ t = esc(rel.display_title()),260+@@ -584,5 +584,9 @@ mod tests {261+ "label {label}"262+ );263+ }264++ // m6/cards2 (f0rge #47): card DOM order is title-first.265++ let title = index.find("data-label=\"title\"").expect("title cell");266++ let ver = index.find("data-label=\"version\"").expect("version cell");267++ assert!(title < ver, "card DOM: title precedes version");268+ }269+ }270+diff --git a/static/style.css b/static/style.css271+index d645df0..a305df3 100644272+--- a/static/style.css273++++ b/static/style.css274+@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}275+ Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.276+ Narrow: no scroll (overflow visible), thead clipped for SR only, each277+ tr becomes a bordered card, each td a labeled flex row via data-label.278+- Title cells lead bold without a repeated label; nothing is deleted. */279++ Title cells lead bold without a repeated label; nothing is deleted.280++ File table exempt (m6/cards2, f0rge #46): dense reference data stays a281++ real table at every width — .file-scroll keeps horizontal scroll and282++ table layout on narrow while every other list table becomes cards. */283+ @media (max-width:767px){284+- .table-scroll{overflow-x:visible}285+- .table-scroll table,.file-table table{width:100%;min-width:0}286+- .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}287+- .table-scroll tbody,.table-scroll tr{display:block;width:auto}288+- .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}289+- .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}290+- .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}291+- .table-scroll td:last-child{border-bottom:none}292+- .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}293+- .table-scroll td.title::before{display:none}294+- .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}295+- .table-scroll td.num,.table-scroll td.sha{width:auto}296+- .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}297+- .table-scroll tbody tr:hover td{background:none}298+- .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}299++ .table-scroll:not(.file-scroll){overflow-x:visible}300++ .table-scroll:not(.file-scroll) table{width:100%;min-width:0}301++ .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}302++ .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}303++ .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}304++ .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}305++ .table-scroll:not(.file-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}306++ .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}307++ .table-scroll:not(.file-scroll) td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}308++ .table-scroll:not(.file-scroll) td.title::before{display:none}309++ .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}310++ .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}311++ .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}312++ .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}313++ .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}314++ .file-scroll{overflow-x:auto}315++ .file-scroll table{width:max-content;min-width:100%}316+ }317+diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs318+index c03348b..b43bfcf 100644319+--- a/tests/forge_readers.rs320++++ b/tests/forge_readers.rs321+@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {322+ 323+ // --- issues index: 2 rendered rows (broken one skipped) ---324+ let issues = read("issues/index.html");325+- let rows = issues.matches("<tr><td class=\"num").count();326++ let rows = issues.matches("<tr><td class=\"title").count();327+ assert_eq!(rows, 2, "rendered issues match file-count minus broken");328+ assert!(!issues.contains("No title here"), "broken issue skipped");329+ // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.330+ assert!(331+ issues.contains(&format!(332+- "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">"333++ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">Older issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"334+ )),335+ "issue #1 -> older uuid"336+ );337+ assert!(338+ issues.contains(&format!(339+- "<td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">"340++ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">Newer issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td>"341+ )),342+ "issue #2 -> newer uuid"343+ );344+@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {345+ // (4): pulls — same shape + head/base/draft/ci + patch <pre>.346+ let pulls = read("pulls/index.html");347+ assert_eq!(348+- pulls.matches("<tr><td class=\"num").count(),349++ pulls.matches("<tr><td class=\"title").count(),350+ 1,351+ "prs match file-count"352+ );353+ assert!(354+ pulls.contains(&format!(355+- "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">"356++ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">Fix the thing</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"357+ )),358+ "pr #1 -> uuid"359+ );360+diff --git a/tests/releases_api.rs b/tests/releases_api.rs361+index 0c7e472..04e6cee 100644362+--- a/tests/releases_api.rs363++++ b/tests/releases_api.rs364+@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {365+ 366+ // --- api counts == html counts (same struct pass) ---367+ let issues_html = read("issues/index.html")368+- .matches("<tr><td class=\"num")369++ .matches("<tr><td class=\"title")370+ .count();371+ let pulls_html = read("pulls/index.html")372+- .matches("<tr><td class=\"num")373++ .matches("<tr><td class=\"title")374++ .count();375++ let commits_html = read("commits.html")376++ .matches("<tr><td class=\"title")377+ .count();378+- let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();379+ let releases_html = read("releases/index.html").matches("<tr><td").count();380+ let wiki_html = read("wiki/index.html")381+ .matches("<li><a href=\"/wiki/")382+diff --git a/tests/truncate.rs b/tests/truncate.rs383+index 12d9004..2f08e2d 100644384+--- a/tests/truncate.rs385++++ b/tests/truncate.rs386+@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {387+ }388+ 389+ fn assert_every_td_truncated(html: &str, page: &str) {390++ // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real391++ // table — trunc + title= kept, no data-label card treatment.392++ assert_every_td_truncated_opts(html, page, page != "index.html");393++}394++395++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {396+ let parts: Vec<&str> = html.split("<td").skip(1).collect();397+ assert!(!parts.is_empty(), "{page} has cells");398+ for (i, p) in parts.iter().enumerate() {399+@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {400+ head.contains("title="),401+ "{page} td #{i} carries title= (got {head:?})"402+ );403++ if expect_labels {404++ assert!(405++ head.contains("data-label="),406++ "{page} td #{i} keeps data-label (got {head:?})"407++ );408++ }409++ }410++ if !expect_labels {411+ assert!(412+- head.contains("data-label="),413+- "{page} td #{i} keeps data-label (got {head:?})"414++ !html.contains("data-label="),415++ "{page} has no card data-labels"416+ );417+ }418+ }419+-- 420+2.55.0421+
.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md+12 −0--- /dev/null+++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md@@ -0,0 +1,12 @@1+---2+id: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+title: m6/cards24+state: closed5+base: master6+head: m6/cards27+draft: false8+issues: []9+ci: null10+created: 2026-09-10T08:57:21.717122363Z11+updated: 2026-09-10T09:01:43.234179940Z12+---
.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff+421 −0--- /dev/null+++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff@@ -0,0 +1,421 @@1+From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 20012+From: gl0w <gl0w@world>3+Date: Thu, 10 Sep 2026 09:57:21 +01004+Subject: [PATCH] m6/cards2: file-table exempt, title-first cards5+6+---7+ src/build.rs | 57 ++++++++++++++++++++++++++----------------8+ src/commits.rs | 12 ++++++---9+ src/forge.rs | 47 +++++++++++++++++++++++++++++-----10+ src/releases.rs | 8 ++++--11+ static/style.css | 37 +++++++++++++++------------12+ tests/forge_readers.rs | 10 ++++----13+ tests/releases_api.rs | 8 +++---14+ tests/truncate.rs | 18 +++++++++++--15+ 8 files changed, 137 insertions(+), 60 deletions(-)16+17+diff --git a/src/build.rs b/src/build.rs18+index 6a12e44..f1c0795 10064419+--- a/src/build.rs20++++ b/src/build.rs21+@@ -579,13 +579,13 @@ pub fn file_table_html(22+ history: &std::collections::BTreeMap<String, (String, String)>,23+ ) -> String {24+ let mut out = String::from(25+- "<div class=\"table-scroll\"><table class=\"file-table\">\n\26++ "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\27+ <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",28+ );29+ if let Some((href, label)) = parent {30+ out.push_str(&format!(31+- "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\32+- <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",33++ "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\34++ <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",35+ esc(label),36+ esc(href),37+ esc(label)38+@@ -601,8 +601,8 @@ pub fn file_table_html(39+ let msg_short = truncate_msg(&msg);40+ if e.is_dir {41+ out.push_str(&format!(42+- "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\43+- <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",44++ "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\45++ <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",46+ esc(&e.name),47+ href_esc(&full),48+ esc(&e.name),49+@@ -613,8 +613,8 @@ pub fn file_table_html(50+ ));51+ } else {52+ out.push_str(&format!(53+- "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\54+- <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",55++ "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\56++ <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",57+ esc(&e.name),58+ href_esc(&full),59+ esc(&e.name),60+@@ -1732,28 +1732,41 @@ mod tests {61+ assert!(sub.contains("title=\""), "title attrs kept");62+ }63+ 64+- /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.65++ /// m6/cards2 (f0rge #46): file table stays a real table on all widths —66++ /// no data-label card treatment, scroll wrapper kept, trunc + title kept.67+ #[test]68+- fn file_table_cells_have_data_labels() {69++ fn file_table_stays_table_no_card_labels() {70+ let css = include_str!("../static/style.css");71+- assert!(css.contains("content:attr(data-label)"), "label rule");72+ assert!(css.contains("@media (max-width:767px)"), "narrow query");73++ assert!(74++ css.contains(".table-scroll:not(.file-scroll)"),75++ "cards scoped off file-scroll"76++ );77++ assert!(78++ css.contains(".file-scroll{overflow-x:auto}"),79++ "file-scroll keeps h-scroll on narrow"80++ );81+ let (_repo, out) = build_fixture();82+ for page in ["index.html", "files/sub.html"] {83+ let html = std::fs::read_to_string(out.join(page)).expect(page);84+- let tds = html.matches("<td").count();85+- assert!(tds > 0, "{page} has cells");86+- assert_eq!(87+- html.matches("data-label=").count(),88+- tds,89+- "{page}: every td labeled"90++ assert!(91++ html.contains(92++ "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"93++ ),94++ "{page}: scroll wrapper kept"95++ );96++ assert!(!html.contains("data-label="), "{page}: no card labels");97++ assert!(98++ html.contains("<td class=\"title trunc trunc-message\""),99++ "{page}: message cell trunc kept"100++ );101++ assert!(html.contains("title=\""), "{page}: title attrs kept");102++ assert!(103++ html.contains("<th>Name</th>")104++ && html.contains("<th>Last commit</th>")105++ && html.contains("<th>Age</th>"),106++ "{page}: headers kept"107+ );108+- for label in ["Name", "Last commit", "Age"] {109+- assert!(110+- html.contains(&format!("data-label=\"{label}\"")),111+- "{page}: label {label}"112+- );113+- }114+ }115+ }116+ 117+diff --git a/src/commits.rs b/src/commits.rs118+index 2324ee1..e3aa5de 100644119+--- a/src/commits.rs120++++ b/src/commits.rs121+@@ -40,15 +40,15 @@ pub fn render_loaded(122+ if log.is_empty() {123+ body.push_str("<p class=\"muted\">No commits yet.</p>\n");124+ } else {125+- 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");126++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>subject</th><th>sha</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");127+ for c in log {128+ body.push_str(&format!(129+- "<tr><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",130++ "<tr><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",131++ esc(&c.subject),132++ esc(&c.subject),133+ esc(&c.sha),134+ esc(&c.sha),135+ esc(short_sha(&c.sha)),136+- esc(&c.subject),137+- esc(&c.subject),138+ esc(&c.author),139+ esc(&c.author),140+ esc(&c.date),141+@@ -423,5 +423,9 @@ mod tests {142+ "label {label}"143+ );144+ }145++ // m6/cards2 (f0rge #47): card DOM order is subject-first.146++ let subj = list.find("data-label=\"subject\"").expect("subject cell");147++ let sha = list.find("data-label=\"sha\"").expect("sha cell");148++ assert!(subj < sha, "card DOM: subject precedes sha");149+ }150+ }151+diff --git a/src/forge.rs b/src/forge.rs152+index 40aa052..d2827b6 100644153+--- a/src/forge.rs154++++ b/src/forge.rs155+@@ -998,10 +998,10 @@ fn render_issues_index(156+ if data.issues.is_empty() {157+ body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");158+ } else {159+- 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");160++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");161+ for issue in &data.issues {162+ body.push_str(&format!(163+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",164++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",165+ n = issue.number,166+ id = esc(&issue.id),167+ title = esc(&issue.title),168+@@ -1059,14 +1059,14 @@ fn render_pulls_index(169+ if prs.is_empty() {170+ body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");171+ } else {172+- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");173++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");174+ for pr in prs {175+ let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));176+ let lanes_plain = format!("{}→{}", pr.head, pr.base);177+ let draft = if pr.draft { " draft" } else { "" };178+ let full_title = format!("{}{}", pr.title, draft);179+ body.push_str(&format!(180+- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",181++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",182+ n = pr.number,183+ id = esc(&pr.id),184+ title = esc(&pr.title),185+@@ -1823,11 +1823,19 @@ mod tests {186+ css.contains("content:attr(data-label)"),187+ "label via ::before"188+ );189+- assert!(css.contains(".table-scroll td::before"), "label rule");190+ assert!(191+- css.contains(".table-scroll td.title::before"),192++ css.contains(".table-scroll:not(.file-scroll) td::before"),193++ "label rule"194++ );195++ assert!(196++ css.contains(".table-scroll:not(.file-scroll) td.title::before"),197+ "title leads card"198+ );199++ // m6/cards2 (f0rge #46): file table exempt from the card reflow.200++ assert!(201++ css.contains(".file-scroll{overflow-x:auto}"),202++ "file table keeps h-scroll on narrow"203++ );204+ assert!(205+ css.contains("display:block") && css.contains("display:flex"),206+ "card layout"207+@@ -1894,8 +1902,35 @@ mod tests {208+ );209+ }210+ }211++ // m6/cards2 (f0rge #47): card DOM order is title-first via template212++ // cell order (no CSS order hacks) — desktop column headers match.213++ for (page, first, second) in [214++ (215++ "issues/index.html",216++ "data-label=\"title\"",217++ "data-label=\"#\"",218++ ),219++ (220++ "pulls/index.html",221++ "data-label=\"title\"",222++ "data-label=\"#\"",223++ ),224++ (225++ "ci/index.html",226++ "data-label=\"step\"",227++ "data-label=\"exit\"",228++ ),229++ ] {230++ let html = std::fs::read_to_string(out.join(page)).expect(page);231++ let a = html.find(first).expect("first cell");232++ let b = html.find(second).expect("second cell");233++ assert!(a < b, "{page}: card DOM title precedes meta");234++ }235+ let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");236+ assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");237++ let title_pos = tasks.find("Card task").expect("task title");238++ let meta_pos = tasks.find("<code>cccccccc").expect("task meta");239++ assert!(title_pos < meta_pos, "tasks: title precedes meta");240+ assert!(tasks.contains("todo (1)"), "tasks group kept");241+ let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");242+ assert!(243+diff --git a/src/releases.rs b/src/releases.rs244+index 68c050b..cceb905 100644245+--- a/src/releases.rs246++++ b/src/releases.rs247+@@ -395,10 +395,10 @@ pub fn render_loaded(248+ if releases.is_empty() {249+ body.push_str("<p class=\"muted\">No releases yet.</p>\n");250+ } else {251+- 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");252++ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>version</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");253+ for rel in releases {254+ body.push_str(&format!(255+- "<tr><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",256++ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",257+ v = esc(&rel.version),258+ ve = esc(&rel.version),259+ t = esc(rel.display_title()),260+@@ -584,5 +584,9 @@ mod tests {261+ "label {label}"262+ );263+ }264++ // m6/cards2 (f0rge #47): card DOM order is title-first.265++ let title = index.find("data-label=\"title\"").expect("title cell");266++ let ver = index.find("data-label=\"version\"").expect("version cell");267++ assert!(title < ver, "card DOM: title precedes version");268+ }269+ }270+diff --git a/static/style.css b/static/style.css271+index d645df0..a305df3 100644272+--- a/static/style.css273++++ b/static/style.css274+@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}275+ Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.276+ Narrow: no scroll (overflow visible), thead clipped for SR only, each277+ tr becomes a bordered card, each td a labeled flex row via data-label.278+- Title cells lead bold without a repeated label; nothing is deleted. */279++ Title cells lead bold without a repeated label; nothing is deleted.280++ File table exempt (m6/cards2, f0rge #46): dense reference data stays a281++ real table at every width — .file-scroll keeps horizontal scroll and282++ table layout on narrow while every other list table becomes cards. */283+ @media (max-width:767px){284+- .table-scroll{overflow-x:visible}285+- .table-scroll table,.file-table table{width:100%;min-width:0}286+- .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}287+- .table-scroll tbody,.table-scroll tr{display:block;width:auto}288+- .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}289+- .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}290+- .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}291+- .table-scroll td:last-child{border-bottom:none}292+- .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}293+- .table-scroll td.title::before{display:none}294+- .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}295+- .table-scroll td.num,.table-scroll td.sha{width:auto}296+- .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}297+- .table-scroll tbody tr:hover td{background:none}298+- .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}299++ .table-scroll:not(.file-scroll){overflow-x:visible}300++ .table-scroll:not(.file-scroll) table{width:100%;min-width:0}301++ .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}302++ .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}303++ .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}304++ .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}305++ .table-scroll:not(.file-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}306++ .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}307++ .table-scroll:not(.file-scroll) td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}308++ .table-scroll:not(.file-scroll) td.title::before{display:none}309++ .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}310++ .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}311++ .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}312++ .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}313++ .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}314++ .file-scroll{overflow-x:auto}315++ .file-scroll table{width:max-content;min-width:100%}316+ }317+diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs318+index c03348b..b43bfcf 100644319+--- a/tests/forge_readers.rs320++++ b/tests/forge_readers.rs321+@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {322+ 323+ // --- issues index: 2 rendered rows (broken one skipped) ---324+ let issues = read("issues/index.html");325+- let rows = issues.matches("<tr><td class=\"num").count();326++ let rows = issues.matches("<tr><td class=\"title").count();327+ assert_eq!(rows, 2, "rendered issues match file-count minus broken");328+ assert!(!issues.contains("No title here"), "broken issue skipped");329+ // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.330+ assert!(331+ issues.contains(&format!(332+- "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">"333++ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">Older issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"334+ )),335+ "issue #1 -> older uuid"336+ );337+ assert!(338+ issues.contains(&format!(339+- "<td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">"340++ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">Newer issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td>"341+ )),342+ "issue #2 -> newer uuid"343+ );344+@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {345+ // (4): pulls — same shape + head/base/draft/ci + patch <pre>.346+ let pulls = read("pulls/index.html");347+ assert_eq!(348+- pulls.matches("<tr><td class=\"num").count(),349++ pulls.matches("<tr><td class=\"title").count(),350+ 1,351+ "prs match file-count"352+ );353+ assert!(354+ pulls.contains(&format!(355+- "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">"356++ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">Fix the thing</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"357+ )),358+ "pr #1 -> uuid"359+ );360+diff --git a/tests/releases_api.rs b/tests/releases_api.rs361+index 0c7e472..04e6cee 100644362+--- a/tests/releases_api.rs363++++ b/tests/releases_api.rs364+@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {365+ 366+ // --- api counts == html counts (same struct pass) ---367+ let issues_html = read("issues/index.html")368+- .matches("<tr><td class=\"num")369++ .matches("<tr><td class=\"title")370+ .count();371+ let pulls_html = read("pulls/index.html")372+- .matches("<tr><td class=\"num")373++ .matches("<tr><td class=\"title")374++ .count();375++ let commits_html = read("commits.html")376++ .matches("<tr><td class=\"title")377+ .count();378+- let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();379+ let releases_html = read("releases/index.html").matches("<tr><td").count();380+ let wiki_html = read("wiki/index.html")381+ .matches("<li><a href=\"/wiki/")382+diff --git a/tests/truncate.rs b/tests/truncate.rs383+index 12d9004..2f08e2d 100644384+--- a/tests/truncate.rs385++++ b/tests/truncate.rs386+@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {387+ }388+ 389+ fn assert_every_td_truncated(html: &str, page: &str) {390++ // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real391++ // table — trunc + title= kept, no data-label card treatment.392++ assert_every_td_truncated_opts(html, page, page != "index.html");393++}394++395++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {396+ let parts: Vec<&str> = html.split("<td").skip(1).collect();397+ assert!(!parts.is_empty(), "{page} has cells");398+ for (i, p) in parts.iter().enumerate() {399+@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {400+ head.contains("title="),401+ "{page} td #{i} carries title= (got {head:?})"402+ );403++ if expect_labels {404++ assert!(405++ head.contains("data-label="),406++ "{page} td #{i} keeps data-label (got {head:?})"407++ );408++ }409++ }410++ if !expect_labels {411+ assert!(412+- head.contains("data-label="),413+- "{page} td #{i} keeps data-label (got {head:?})"414++ !html.contains("data-label="),415++ "{page} has no card data-labels"416+ );417+ }418+ }419+-- 420+2.55.0421+
.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md+12 −0--- /dev/null+++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md@@ -0,0 +1,12 @@1+---2+id: 1329e344-0a57-4508-aecc-aca2914005be3+title: m6/cards24+state: open5+base: master6+head: m6/cards27+draft: false8+issues: []9+ci: null10+created: 2026-09-10T09:01:43.245267981Z11+updated: null12+---
src/build.rs+35 −22--- a/src/build.rs+++ b/src/build.rs@@ -579,13 +579,13 @@ pub fn file_table_html(579579 history: &std::collections::BTreeMap<String, (String, String)>,580580 ) -> String {581581 let mut out = String::from(582- "<div class=\"table-scroll\"><table class=\"file-table\">\n\582+ "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\583583 <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",584584 );585585 if let Some((href, label)) = parent {586586 out.push_str(&format!(587- "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\588- <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",587+ "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\588+ <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",589589 esc(label),590590 esc(href),591591 esc(label)@@ -601,8 +601,8 @@ pub fn file_table_html(601601 let msg_short = truncate_msg(&msg);602602 if e.is_dir {603603 out.push_str(&format!(604- "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\605- <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",604+ "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\605+ <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",606606 esc(&e.name),607607 href_esc(&full),608608 esc(&e.name),@@ -613,8 +613,8 @@ pub fn file_table_html(613613 ));614614 } else {615615 out.push_str(&format!(616- "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\617- <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",616+ "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\617+ <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",618618 esc(&e.name),619619 href_esc(&full),620620 esc(&e.name),@@ -1732,28 +1732,41 @@ mod tests {17321732 assert!(sub.contains("title=\""), "title attrs kept");17331733 }17341734 1735- /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.1735+ /// m6/cards2 (f0rge #46): file table stays a real table on all widths —1736+ /// no data-label card treatment, scroll wrapper kept, trunc + title kept.17361737 #[test]1737- fn file_table_cells_have_data_labels() {1738+ fn file_table_stays_table_no_card_labels() {17381739 let css = include_str!("../static/style.css");1739- assert!(css.contains("content:attr(data-label)"), "label rule");17401740 assert!(css.contains("@media (max-width:767px)"), "narrow query");1741+ assert!(1742+ css.contains(".table-scroll:not(.file-scroll)"),1743+ "cards scoped off file-scroll"1744+ );1745+ assert!(1746+ css.contains(".file-scroll{overflow-x:auto}"),1747+ "file-scroll keeps h-scroll on narrow"1748+ );17411749 let (_repo, out) = build_fixture();17421750 for page in ["index.html", "files/sub.html"] {17431751 let html = std::fs::read_to_string(out.join(page)).expect(page);1744- let tds = html.matches("<td").count();1745- assert!(tds > 0, "{page} has cells");1746- assert_eq!(1747- html.matches("data-label=").count(),1748- tds,1749- "{page}: every td labeled"1752+ assert!(1753+ html.contains(1754+ "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"1755+ ),1756+ "{page}: scroll wrapper kept"1757+ );1758+ assert!(!html.contains("data-label="), "{page}: no card labels");1759+ assert!(1760+ html.contains("<td class=\"title trunc trunc-message\""),1761+ "{page}: message cell trunc kept"1762+ );1763+ assert!(html.contains("title=\""), "{page}: title attrs kept");1764+ assert!(1765+ html.contains("<th>Name</th>")1766+ && html.contains("<th>Last commit</th>")1767+ && html.contains("<th>Age</th>"),1768+ "{page}: headers kept"17501769 );1751- for label in ["Name", "Last commit", "Age"] {1752- assert!(1753- html.contains(&format!("data-label=\"{label}\"")),1754- "{page}: label {label}"1755- );1756- }17571770 }17581771 }17591772
src/commits.rs+8 −4--- a/src/commits.rs+++ b/src/commits.rs@@ -40,15 +40,15 @@ pub fn render_loaded(4040 if log.is_empty() {4141 body.push_str("<p class=\"muted\">No commits yet.</p>\n");4242 } else {43- 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");43+ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>subject</th><th>sha</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");4444 for c in log {4545 body.push_str(&format!(46- "<tr><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",46+ "<tr><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",47+ esc(&c.subject),48+ esc(&c.subject),4749 esc(&c.sha),4850 esc(&c.sha),4951 esc(short_sha(&c.sha)),50- esc(&c.subject),51- esc(&c.subject),5252 esc(&c.author),5353 esc(&c.author),5454 esc(&c.date),@@ -423,5 +423,9 @@ mod tests {423423 "label {label}"424424 );425425 }426+ // m6/cards2 (f0rge #47): card DOM order is subject-first.427+ let subj = list.find("data-label=\"subject\"").expect("subject cell");428+ let sha = list.find("data-label=\"sha\"").expect("sha cell");429+ assert!(subj < sha, "card DOM: subject precedes sha");426430 }427431 }
src/forge.rs+43 −6--- a/src/forge.rs+++ b/src/forge.rs@@ -1052,12 +1052,13 @@ fn render_issues_index(10521052 if data.issues.is_empty() {10531053 body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");10541054 } else {1055- 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");1055+ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");10561056 // m6/sort (#48 + #49): open first (newest-first within), then10571057 // non-open (newest-first within). Numbers stay creation-rank.1058+ // m6/cards2 (#46 + #47): title-first column order.10581059 for issue in data.issues_in_display_order() {10591060 body.push_str(&format!(1060- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",1061+ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",10611062 n = issue.number,10621063 id = esc(&issue.id),10631064 title = esc(&issue.title),@@ -1114,17 +1115,18 @@ fn render_pulls_index(11141115 if data.prs.is_empty() {11151116 body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");11161117 } else {1117- body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");1118+ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");11181119 // m6/sort (#48 + #49): open (incl drafts) first (newest-first11191120 // within), then closed/merged (newest-first within).11201121 // Numbers stay creation-rank.1122+ // m6/cards2 (#46 + #47): title-first column order.11211123 for pr in data.prs_in_display_order() {11221124 let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));11231125 let lanes_plain = format!("{}→{}", pr.head, pr.base);11241126 let draft = if pr.draft { " draft" } else { "" };11251127 let full_title = format!("{}{}", pr.title, draft);11261128 body.push_str(&format!(1127- "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",1129+ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",11281130 n = pr.number,11291131 id = esc(&pr.id),11301132 title = esc(&pr.title),@@ -2073,11 +2075,19 @@ mod tests {20732075 css.contains("content:attr(data-label)"),20742076 "label via ::before"20752077 );2076- assert!(css.contains(".table-scroll td::before"), "label rule");20772078 assert!(2078- css.contains(".table-scroll td.title::before"),2079+ css.contains(".table-scroll:not(.file-scroll) td::before"),2080+ "label rule"2081+ );2082+ assert!(2083+ css.contains(".table-scroll:not(.file-scroll) td.title::before"),20792084 "title leads card"20802085 );2086+ // m6/cards2 (f0rge #46): file table exempt from the card reflow.2087+ assert!(2088+ css.contains(".file-scroll{overflow-x:auto}"),2089+ "file table keeps h-scroll on narrow"2090+ );20812091 assert!(20822092 css.contains("display:block") && css.contains("display:flex"),20832093 "card layout"@@ -2144,8 +2154,35 @@ mod tests {21442154 );21452155 }21462156 }2157+ // m6/cards2 (f0rge #47): card DOM order is title-first via template2158+ // cell order (no CSS order hacks) — desktop column headers match.2159+ for (page, first, second) in [2160+ (2161+ "issues/index.html",2162+ "data-label=\"title\"",2163+ "data-label=\"#\"",2164+ ),2165+ (2166+ "pulls/index.html",2167+ "data-label=\"title\"",2168+ "data-label=\"#\"",2169+ ),2170+ (2171+ "ci/index.html",2172+ "data-label=\"step\"",2173+ "data-label=\"exit\"",2174+ ),2175+ ] {2176+ let html = std::fs::read_to_string(out.join(page)).expect(page);2177+ let a = html.find(first).expect("first cell");2178+ let b = html.find(second).expect("second cell");2179+ assert!(a < b, "{page}: card DOM title precedes meta");2180+ }21472181 let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");21482182 assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");2183+ let title_pos = tasks.find("Card task").expect("task title");2184+ let meta_pos = tasks.find("<code>cccccccc").expect("task meta");2185+ assert!(title_pos < meta_pos, "tasks: title precedes meta");21492186 assert!(tasks.contains("todo (1)"), "tasks group kept");21502187 let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");21512188 assert!(
src/releases.rs+6 −2--- a/src/releases.rs+++ b/src/releases.rs@@ -395,10 +395,10 @@ pub fn render_loaded(395395 if releases.is_empty() {396396 body.push_str("<p class=\"muted\">No releases yet.</p>\n");397397 } else {398- 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");398+ body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>version</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");399399 for rel in releases {400400 body.push_str(&format!(401- "<tr><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",401+ "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",402402 v = esc(&rel.version),403403 ve = esc(&rel.version),404404 t = esc(rel.display_title()),@@ -584,5 +584,9 @@ mod tests {584584 "label {label}"585585 );586586 }587+ // m6/cards2 (f0rge #47): card DOM order is title-first.588+ let title = index.find("data-label=\"title\"").expect("title cell");589+ let ver = index.find("data-label=\"version\"").expect("version cell");590+ assert!(title < ver, "card DOM: title precedes version");587591 }588592 }
static/style.css+21 −16--- a/static/style.css+++ b/static/style.css@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}229229 Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.230230 Narrow: no scroll (overflow visible), thead clipped for SR only, each231231 tr becomes a bordered card, each td a labeled flex row via data-label.232- Title cells lead bold without a repeated label; nothing is deleted. */232+ Title cells lead bold without a repeated label; nothing is deleted.233+ File table exempt (m6/cards2, f0rge #46): dense reference data stays a234+ real table at every width — .file-scroll keeps horizontal scroll and235+ table layout on narrow while every other list table becomes cards. */233236 @media (max-width:767px){234- .table-scroll{overflow-x:visible}235- .table-scroll table,.file-table table{width:100%;min-width:0}236- .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}237- .table-scroll tbody,.table-scroll tr{display:block;width:auto}238- .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}239- .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}240- .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}241- .table-scroll td:last-child{border-bottom:none}242- .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}243- .table-scroll td.title::before{display:none}244- .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}245- .table-scroll td.num,.table-scroll td.sha{width:auto}246- .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}247- .table-scroll tbody tr:hover td{background:none}248- .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}237+ .table-scroll:not(.file-scroll){overflow-x:visible}238+ .table-scroll:not(.file-scroll) table{width:100%;min-width:0}239+ .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}240+ .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}241+ .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}242+ .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}243+ .table-scroll:not(.file-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}244+ .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}245+ .table-scroll:not(.file-scroll) td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}246+ .table-scroll:not(.file-scroll) td.title::before{display:none}247+ .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}248+ .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}249+ .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}250+ .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}251+ .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}252+ .file-scroll{overflow-x:auto}253+ .file-scroll table{width:max-content;min-width:100%}249254 }
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=\"title").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 trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">"233+ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">Older issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"234234 )),235235 "issue #1 -> older uuid"236236 );237237 assert!(238238 issues.contains(&format!(239- "<td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">"239+ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">Newer issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td>"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=\"title").count(),267267 1,268268 "prs match file-count"269269 );270270 assert!(271271 pulls.contains(&format!(272- "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">"272+ "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">Fix the thing</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"273273 )),274274 "pr #1 -> uuid"275275 );
tests/releases_api.rs+5 −3--- a/tests/releases_api.rs+++ b/tests/releases_api.rs@@ -154,12 +154,14 @@ 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=\"title")158158 .count();159159 let pulls_html = read("pulls/index.html")160- .matches("<tr><td class=\"num")160+ .matches("<tr><td class=\"title")161+ .count();162+ let commits_html = read("commits.html")163+ .matches("<tr><td class=\"title")161164 .count();162- let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();163165 let releases_html = read("releases/index.html").matches("<tr><td").count();164166 let wiki_html = read("wiki/index.html")165167 .matches("<li><a href=\"/wiki/")
tests/truncate.rs+16 −3--- a/tests/truncate.rs+++ b/tests/truncate.rs@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {135135 }136136 137137 fn assert_every_td_truncated(html: &str, page: &str) {138+ // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real139+ // table — trunc + title= kept, no data-label card treatment.140+ assert_every_td_truncated_opts(html, page, page != "index.html");141+}142+143+fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {138144 let parts: Vec<&str> = html.split("<td").skip(1).collect();139145 assert!(!parts.is_empty(), "{page} has cells");140146 for (i, p) in parts.iter().enumerate() {@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {147153 head.contains("title="),148154 "{page} td #{i} carries title= (got {head:?})"149155 );156+ if expect_labels {157+ assert!(158+ head.contains("data-label="),159+ "{page} td #{i} keeps data-label (got {head:?})"160+ );161+ }162+ }163+ if !expect_labels {150164 assert!(151- head.contains("data-label="),152- "{page} td #{i} keeps data-label (got {head:?})"165+ !html.contains("data-label="),166+ "{page} has no card data-labels"153167 );154168 }155169 }156--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.