.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/patch.diff 94.1 KiB 1595 lines Raw
1 From ff259a6d4be192939058841d3d1d829b65fac1bc Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 10:02:02 +01004 Subject: [PATCH] m6/cards2: file-table exempt, title-first cards (+sort5  compat)6 7 ---8  .../merges/2026-09-10T08-57-21.752386694Z.md  |  59 +++9  .../merges/2026-09-10T08-58-33.103715749Z.md  |  59 +++10  .../merges/2026-09-10T09-01-18.492745439Z.md  |  59 +++11  .../merges/2026-09-10T09-01-43.282852796Z.md  |  59 +++12  .../patch.diff                                | 421 ++++++++++++++++++13  .../pr.md                                     |  12 +14  .../patch.diff                                | 421 ++++++++++++++++++15  .../pr.md                                     |  12 +16  src/build.rs                                  |  57 ++-17  src/commits.rs                                |  12 +-18  src/forge.rs                                  |  49 +-19  src/releases.rs                               |   8 +-20  static/style.css                              |  37 +-21  tests/forge_readers.rs                        |  10 +-22  tests/releases_api.rs                         |   8 +-23  tests/truncate.rs                             |  18 +-24  16 files changed, 1241 insertions(+), 60 deletions(-)25  create mode 100644 .forge/merges/2026-09-10T08-57-21.752386694Z.md26  create mode 100644 .forge/merges/2026-09-10T08-58-33.103715749Z.md27  create mode 100644 .forge/merges/2026-09-10T09-01-18.492745439Z.md28  create mode 100644 .forge/merges/2026-09-10T09-01-43.282852796Z.md29  create mode 100644 .forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff30  create mode 100644 .forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md31  create mode 100644 .forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff32  create mode 100644 .forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md33 34 diff --git a/.forge/merges/2026-09-10T08-57-21.752386694Z.md b/.forge/merges/2026-09-10T08-57-21.752386694Z.md35 new file mode 10064436 index 0000000..2a667b037 --- /dev/null38 +++ b/.forge/merges/2026-09-10T08-57-21.752386694Z.md39 @@ -0,0 +1,59 @@40 +---41 +pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae42 +applied: false43 +verdict: fail44 +resolved: []45 +created: 2026-09-10T08:57:21.752386694Z46 +---47 +conflict: src/forge.rs48 +patch: 67935671dbaf97887378ed0535994bf84da082a649 +---50 +file: src/forge.rs51 +base master:src/forge.rs:52 +<too large on base>53 +head m6/cards2:src/forge.rs:54 +<too large on head>55 +patch:56 +diff --git a/src/forge.rs b/src/forge.rs57 +index 40aa052..d2827b6 10064458 +--- a/src/forge.rs59 ++++ b/src/forge.rs60 +@@ -998,10 +998,10 @@ fn render_issues_index(61 +     if data.issues.is_empty() {62 +         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");63 +     } else {64 +-        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");65 ++        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");66 +         for issue in &data.issues {67 +             body.push_str(&format!(68 +-                "<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",69 ++                "<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",70 +                 n = issue.number,71 +                 id = esc(&issue.id),72 +                 title = esc(&issue.title),73 +@@ -1059,14 +1059,14 @@ fn render_pulls_index(74 +     if prs.is_empty() {75 +         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");76 +     } else {77 +-        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");78 ++        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");79 +         for pr in prs {80 +             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));81 +             let lanes_plain = format!("{}→{}", pr.head, pr.base);82 +             let draft = if pr.draft { " draft" } else { "" };83 +             let full_title = format!("{}{}", pr.title, draft);84 +             body.push_str(&format!(85 +-                "<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",86 ++                "<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",87 +                 n = pr.number,88 +                 id = esc(&pr.id),89 +                 title = esc(&pr.title),90 +@@ -1823,11 +1823,19 @@ mod tests {91 +             css.contains("content:attr(data-label)"),92 +             "label via ::before"93 +         );94 +-        assert!(css.contains(".table-scroll td::before"), "label rule");95 +         assert!(96 +-            css.contains(".table-scroll td.title::before"),97 ++ 98 +...truncated99 diff --git a/.forge/merges/2026-09-10T08-58-33.103715749Z.md b/.forge/merges/2026-09-10T08-58-33.103715749Z.md100 new file mode 100644101 index 0000000..040e3cf102 --- /dev/null103 +++ b/.forge/merges/2026-09-10T08-58-33.103715749Z.md104 @@ -0,0 +1,59 @@105 +---106 +pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae107 +applied: false108 +verdict: fail109 +resolved: []110 +created: 2026-09-10T08:58:33.103715749Z111 +---112 +conflict: src/forge.rs113 +patch: 67935671dbaf97887378ed0535994bf84da082a6114 +---115 +file: src/forge.rs116 +base master:src/forge.rs:117 +<too large on base>118 +head m6/cards2:src/forge.rs:119 +<too large on head>120 +patch:121 +diff --git a/src/forge.rs b/src/forge.rs122 +index 40aa052..d2827b6 100644123 +--- a/src/forge.rs124 ++++ b/src/forge.rs125 +@@ -998,10 +998,10 @@ fn render_issues_index(126 +     if data.issues.is_empty() {127 +         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");128 +     } else {129 +-        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");130 ++        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");131 +         for issue in &data.issues {132 +             body.push_str(&format!(133 +-                "<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",134 ++                "<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",135 +                 n = issue.number,136 +                 id = esc(&issue.id),137 +                 title = esc(&issue.title),138 +@@ -1059,14 +1059,14 @@ fn render_pulls_index(139 +     if prs.is_empty() {140 +         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");141 +     } else {142 +-        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");143 ++        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");144 +         for pr in prs {145 +             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));146 +             let lanes_plain = format!("{}→{}", pr.head, pr.base);147 +             let draft = if pr.draft { " draft" } else { "" };148 +             let full_title = format!("{}{}", pr.title, draft);149 +             body.push_str(&format!(150 +-                "<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",151 ++                "<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",152 +                 n = pr.number,153 +                 id = esc(&pr.id),154 +                 title = esc(&pr.title),155 +@@ -1823,11 +1823,19 @@ mod tests {156 +             css.contains("content:attr(data-label)"),157 +             "label via ::before"158 +         );159 +-        assert!(css.contains(".table-scroll td::before"), "label rule");160 +         assert!(161 +-            css.contains(".table-scroll td.title::before"),162 ++ 163 +...truncated164 diff --git a/.forge/merges/2026-09-10T09-01-18.492745439Z.md b/.forge/merges/2026-09-10T09-01-18.492745439Z.md165 new file mode 100644166 index 0000000..175b82c167 --- /dev/null168 +++ b/.forge/merges/2026-09-10T09-01-18.492745439Z.md169 @@ -0,0 +1,59 @@170 +---171 +pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae172 +applied: false173 +verdict: fail174 +resolved: []175 +created: 2026-09-10T09:01:18.492745439Z176 +---177 +conflict: src/forge.rs178 +patch: 67935671dbaf97887378ed0535994bf84da082a6179 +---180 +file: src/forge.rs181 +base master:src/forge.rs:182 +<too large on base>183 +head m6/cards2:src/forge.rs:184 +<too large on head>185 +patch:186 +diff --git a/src/forge.rs b/src/forge.rs187 +index 40aa052..d2827b6 100644188 +--- a/src/forge.rs189 ++++ b/src/forge.rs190 +@@ -998,10 +998,10 @@ fn render_issues_index(191 +     if data.issues.is_empty() {192 +         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");193 +     } else {194 +-        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");195 ++        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");196 +         for issue in &data.issues {197 +             body.push_str(&format!(198 +-                "<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",199 ++                "<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",200 +                 n = issue.number,201 +                 id = esc(&issue.id),202 +                 title = esc(&issue.title),203 +@@ -1059,14 +1059,14 @@ fn render_pulls_index(204 +     if prs.is_empty() {205 +         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");206 +     } else {207 +-        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");208 ++        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");209 +         for pr in prs {210 +             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));211 +             let lanes_plain = format!("{}→{}", pr.head, pr.base);212 +             let draft = if pr.draft { " draft" } else { "" };213 +             let full_title = format!("{}{}", pr.title, draft);214 +             body.push_str(&format!(215 +-                "<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",216 ++                "<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",217 +                 n = pr.number,218 +                 id = esc(&pr.id),219 +                 title = esc(&pr.title),220 +@@ -1823,11 +1823,19 @@ mod tests {221 +             css.contains("content:attr(data-label)"),222 +             "label via ::before"223 +         );224 +-        assert!(css.contains(".table-scroll td::before"), "label rule");225 +         assert!(226 +-            css.contains(".table-scroll td.title::before"),227 ++ 228 +...truncated229 diff --git a/.forge/merges/2026-09-10T09-01-43.282852796Z.md b/.forge/merges/2026-09-10T09-01-43.282852796Z.md230 new file mode 100644231 index 0000000..c982aa0232 --- /dev/null233 +++ b/.forge/merges/2026-09-10T09-01-43.282852796Z.md234 @@ -0,0 +1,59 @@235 +---236 +pr: 1329e344-0a57-4508-aecc-aca2914005be237 +applied: false238 +verdict: fail239 +resolved: []240 +created: 2026-09-10T09:01:43.282852796Z241 +---242 +conflict: src/forge.rs243 +patch: 67935671dbaf97887378ed0535994bf84da082a6244 +---245 +file: src/forge.rs246 +base master:src/forge.rs:247 +<too large on base>248 +head m6/cards2:src/forge.rs:249 +<too large on head>250 +patch:251 +diff --git a/src/forge.rs b/src/forge.rs252 +index 40aa052..d2827b6 100644253 +--- a/src/forge.rs254 ++++ b/src/forge.rs255 +@@ -998,10 +998,10 @@ fn render_issues_index(256 +     if data.issues.is_empty() {257 +         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");258 +     } else {259 +-        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");260 ++        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");261 +         for issue in &data.issues {262 +             body.push_str(&format!(263 +-                "<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",264 ++                "<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",265 +                 n = issue.number,266 +                 id = esc(&issue.id),267 +                 title = esc(&issue.title),268 +@@ -1059,14 +1059,14 @@ fn render_pulls_index(269 +     if prs.is_empty() {270 +         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");271 +     } else {272 +-        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");273 ++        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");274 +         for pr in prs {275 +             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));276 +             let lanes_plain = format!("{}→{}", pr.head, pr.base);277 +             let draft = if pr.draft { " draft" } else { "" };278 +             let full_title = format!("{}{}", pr.title, draft);279 +             body.push_str(&format!(280 +-                "<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",281 ++                "<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",282 +                 n = pr.number,283 +                 id = esc(&pr.id),284 +                 title = esc(&pr.title),285 +@@ -1823,11 +1823,19 @@ mod tests {286 +             css.contains("content:attr(data-label)"),287 +             "label via ::before"288 +         );289 +-        assert!(css.contains(".table-scroll td::before"), "label rule");290 +         assert!(291 +-            css.contains(".table-scroll td.title::before"),292 ++ 293 +...truncated294 diff --git a/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff295 new file mode 100644296 index 0000000..99535be297 --- /dev/null298 +++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff299 @@ -0,0 +1,421 @@300 +From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 2001301 +From: gl0w <gl0w@world>302 +Date: Thu, 10 Sep 2026 09:57:21 +0100303 +Subject: [PATCH] m6/cards2: file-table exempt, title-first cards304 +305 +---306 + src/build.rs           | 57 ++++++++++++++++++++++++++----------------307 + src/commits.rs         | 12 ++++++---308 + src/forge.rs           | 47 +++++++++++++++++++++++++++++-----309 + src/releases.rs        |  8 ++++--310 + static/style.css       | 37 +++++++++++++++------------311 + tests/forge_readers.rs | 10 ++++----312 + tests/releases_api.rs  |  8 +++---313 + tests/truncate.rs      | 18 +++++++++++--314 + 8 files changed, 137 insertions(+), 60 deletions(-)315 +316 +diff --git a/src/build.rs b/src/build.rs317 +index 6a12e44..f1c0795 100644318 +--- a/src/build.rs319 ++++ b/src/build.rs320 +@@ -579,13 +579,13 @@ pub fn file_table_html(321 +     history: &std::collections::BTreeMap<String, (String, String)>,322 + ) -> String {323 +     let mut out = String::from(324 +-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\325 ++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\326 +          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",327 +     );328 +     if let Some((href, label)) = parent {329 +         out.push_str(&format!(330 +-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\331 +-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",332 ++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\333 ++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",334 +             esc(label),335 +             esc(href),336 +             esc(label)337 +@@ -601,8 +601,8 @@ pub fn file_table_html(338 +         let msg_short = truncate_msg(&msg);339 +         if e.is_dir {340 +             out.push_str(&format!(341 +-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\342 +-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",343 ++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\344 ++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",345 +                 esc(&e.name),346 +                 href_esc(&full),347 +                 esc(&e.name),348 +@@ -613,8 +613,8 @@ pub fn file_table_html(349 +             ));350 +         } else {351 +             out.push_str(&format!(352 +-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\353 +-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",354 ++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\355 ++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",356 +                 esc(&e.name),357 +                 href_esc(&full),358 +                 esc(&e.name),359 +@@ -1732,28 +1732,41 @@ mod tests {360 +         assert!(sub.contains("title=\""), "title attrs kept");361 +     }362 + 363 +-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.364 ++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —365 ++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.366 +     #[test]367 +-    fn file_table_cells_have_data_labels() {368 ++    fn file_table_stays_table_no_card_labels() {369 +         let css = include_str!("../static/style.css");370 +-        assert!(css.contains("content:attr(data-label)"), "label rule");371 +         assert!(css.contains("@media (max-width:767px)"), "narrow query");372 ++        assert!(373 ++            css.contains(".table-scroll:not(.file-scroll)"),374 ++            "cards scoped off file-scroll"375 ++        );376 ++        assert!(377 ++            css.contains(".file-scroll{overflow-x:auto}"),378 ++            "file-scroll keeps h-scroll on narrow"379 ++        );380 +         let (_repo, out) = build_fixture();381 +         for page in ["index.html", "files/sub.html"] {382 +             let html = std::fs::read_to_string(out.join(page)).expect(page);383 +-            let tds = html.matches("<td").count();384 +-            assert!(tds > 0, "{page} has cells");385 +-            assert_eq!(386 +-                html.matches("data-label=").count(),387 +-                tds,388 +-                "{page}: every td labeled"389 ++            assert!(390 ++                html.contains(391 ++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"392 ++                ),393 ++                "{page}: scroll wrapper kept"394 ++            );395 ++            assert!(!html.contains("data-label="), "{page}: no card labels");396 ++            assert!(397 ++                html.contains("<td class=\"title trunc trunc-message\""),398 ++                "{page}: message cell trunc kept"399 ++            );400 ++            assert!(html.contains("title=\""), "{page}: title attrs kept");401 ++            assert!(402 ++                html.contains("<th>Name</th>")403 ++                    && html.contains("<th>Last commit</th>")404 ++                    && html.contains("<th>Age</th>"),405 ++                "{page}: headers kept"406 +             );407 +-            for label in ["Name", "Last commit", "Age"] {408 +-                assert!(409 +-                    html.contains(&format!("data-label=\"{label}\"")),410 +-                    "{page}: label {label}"411 +-                );412 +-            }413 +         }414 +     }415 + 416 +diff --git a/src/commits.rs b/src/commits.rs417 +index 2324ee1..e3aa5de 100644418 +--- a/src/commits.rs419 ++++ b/src/commits.rs420 +@@ -40,15 +40,15 @@ pub fn render_loaded(421 +     if log.is_empty() {422 +         body.push_str("<p class=\"muted\">No commits yet.</p>\n");423 +     } else {424 +-        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");425 ++        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");426 +         for c in log {427 +             body.push_str(&format!(428 +-                "<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",429 ++                "<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",430 ++                esc(&c.subject),431 ++                esc(&c.subject),432 +                 esc(&c.sha),433 +                 esc(&c.sha),434 +                 esc(short_sha(&c.sha)),435 +-                esc(&c.subject),436 +-                esc(&c.subject),437 +                 esc(&c.author),438 +                 esc(&c.author),439 +                 esc(&c.date),440 +@@ -423,5 +423,9 @@ mod tests {441 +                 "label {label}"442 +             );443 +         }444 ++        // m6/cards2 (f0rge #47): card DOM order is subject-first.445 ++        let subj = list.find("data-label=\"subject\"").expect("subject cell");446 ++        let sha = list.find("data-label=\"sha\"").expect("sha cell");447 ++        assert!(subj < sha, "card DOM: subject precedes sha");448 +     }449 + }450 +diff --git a/src/forge.rs b/src/forge.rs451 +index 40aa052..d2827b6 100644452 +--- a/src/forge.rs453 ++++ b/src/forge.rs454 +@@ -998,10 +998,10 @@ fn render_issues_index(455 +     if data.issues.is_empty() {456 +         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");457 +     } else {458 +-        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");459 ++        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");460 +         for issue in &data.issues {461 +             body.push_str(&format!(462 +-                "<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",463 ++                "<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",464 +                 n = issue.number,465 +                 id = esc(&issue.id),466 +                 title = esc(&issue.title),467 +@@ -1059,14 +1059,14 @@ fn render_pulls_index(468 +     if prs.is_empty() {469 +         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");470 +     } else {471 +-        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");472 ++        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");473 +         for pr in prs {474 +             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));475 +             let lanes_plain = format!("{}→{}", pr.head, pr.base);476 +             let draft = if pr.draft { " draft" } else { "" };477 +             let full_title = format!("{}{}", pr.title, draft);478 +             body.push_str(&format!(479 +-                "<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",480 ++                "<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",481 +                 n = pr.number,482 +                 id = esc(&pr.id),483 +                 title = esc(&pr.title),484 +@@ -1823,11 +1823,19 @@ mod tests {485 +             css.contains("content:attr(data-label)"),486 +             "label via ::before"487 +         );488 +-        assert!(css.contains(".table-scroll td::before"), "label rule");489 +         assert!(490 +-            css.contains(".table-scroll td.title::before"),491 ++            css.contains(".table-scroll:not(.file-scroll) td::before"),492 ++            "label rule"493 ++        );494 ++        assert!(495 ++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),496 +             "title leads card"497 +         );498 ++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.499 ++        assert!(500 ++            css.contains(".file-scroll{overflow-x:auto}"),501 ++            "file table keeps h-scroll on narrow"502 ++        );503 +         assert!(504 +             css.contains("display:block") && css.contains("display:flex"),505 +             "card layout"506 +@@ -1894,8 +1902,35 @@ mod tests {507 +                 );508 +             }509 +         }510 ++        // m6/cards2 (f0rge #47): card DOM order is title-first via template511 ++        // cell order (no CSS order hacks) — desktop column headers match.512 ++        for (page, first, second) in [513 ++            (514 ++                "issues/index.html",515 ++                "data-label=\"title\"",516 ++                "data-label=\"#\"",517 ++            ),518 ++            (519 ++                "pulls/index.html",520 ++                "data-label=\"title\"",521 ++                "data-label=\"#\"",522 ++            ),523 ++            (524 ++                "ci/index.html",525 ++                "data-label=\"step\"",526 ++                "data-label=\"exit\"",527 ++            ),528 ++        ] {529 ++            let html = std::fs::read_to_string(out.join(page)).expect(page);530 ++            let a = html.find(first).expect("first cell");531 ++            let b = html.find(second).expect("second cell");532 ++            assert!(a < b, "{page}: card DOM title precedes meta");533 ++        }534 +         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");535 +         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");536 ++        let title_pos = tasks.find("Card task").expect("task title");537 ++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");538 ++        assert!(title_pos < meta_pos, "tasks: title precedes meta");539 +         assert!(tasks.contains("todo (1)"), "tasks group kept");540 +         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");541 +         assert!(542 +diff --git a/src/releases.rs b/src/releases.rs543 +index 68c050b..cceb905 100644544 +--- a/src/releases.rs545 ++++ b/src/releases.rs546 +@@ -395,10 +395,10 @@ pub fn render_loaded(547 +     if releases.is_empty() {548 +         body.push_str("<p class=\"muted\">No releases yet.</p>\n");549 +     } else {550 +-        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");551 ++        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");552 +         for rel in releases {553 +             body.push_str(&format!(554 +-                "<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",555 ++                "<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",556 +                 v = esc(&rel.version),557 +                 ve = esc(&rel.version),558 +                 t = esc(rel.display_title()),559 +@@ -584,5 +584,9 @@ mod tests {560 +                 "label {label}"561 +             );562 +         }563 ++        // m6/cards2 (f0rge #47): card DOM order is title-first.564 ++        let title = index.find("data-label=\"title\"").expect("title cell");565 ++        let ver = index.find("data-label=\"version\"").expect("version cell");566 ++        assert!(title < ver, "card DOM: title precedes version");567 +     }568 + }569 +diff --git a/static/style.css b/static/style.css570 +index d645df0..a305df3 100644571 +--- a/static/style.css572 ++++ b/static/style.css573 +@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}574 +    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.575 +    Narrow: no scroll (overflow visible), thead clipped for SR only, each576 +    tr becomes a bordered card, each td a labeled flex row via data-label.577 +-   Title cells lead bold without a repeated label; nothing is deleted. */578 ++   Title cells lead bold without a repeated label; nothing is deleted.579 ++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a580 ++   real table at every width — .file-scroll keeps horizontal scroll and581 ++   table layout on narrow while every other list table becomes cards. */582 + @media (max-width:767px){583 +-  .table-scroll{overflow-x:visible}584 +-  .table-scroll table,.file-table table{width:100%;min-width:0}585 +-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}586 +-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}587 +-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}588 +-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}589 +-  .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}590 +-  .table-scroll td:last-child{border-bottom:none}591 +-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}592 +-  .table-scroll td.title::before{display:none}593 +-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}594 +-  .table-scroll td.num,.table-scroll td.sha{width:auto}595 +-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}596 +-  .table-scroll tbody tr:hover td{background:none}597 +-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}598 ++  .table-scroll:not(.file-scroll){overflow-x:visible}599 ++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}600 ++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}601 ++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}602 ++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}603 ++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}604 ++  .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}605 ++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}606 ++  .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}607 ++  .table-scroll:not(.file-scroll) td.title::before{display:none}608 ++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}609 ++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}610 ++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}611 ++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}612 ++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}613 ++  .file-scroll{overflow-x:auto}614 ++  .file-scroll table{width:max-content;min-width:100%}615 + }616 +diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs617 +index c03348b..b43bfcf 100644618 +--- a/tests/forge_readers.rs619 ++++ b/tests/forge_readers.rs620 +@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {621 + 622 +     // --- issues index: 2 rendered rows (broken one skipped) ---623 +     let issues = read("issues/index.html");624 +-    let rows = issues.matches("<tr><td class=\"num").count();625 ++    let rows = issues.matches("<tr><td class=\"title").count();626 +     assert_eq!(rows, 2, "rendered issues match file-count minus broken");627 +     assert!(!issues.contains("No title here"), "broken issue skipped");628 +     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.629 +     assert!(630 +         issues.contains(&format!(631 +-            "<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\">"632 ++            "<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>"633 +         )),634 +         "issue #1 -> older uuid"635 +     );636 +     assert!(637 +         issues.contains(&format!(638 +-            "<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\">"639 ++            "<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>"640 +         )),641 +         "issue #2 -> newer uuid"642 +     );643 +@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {644 +     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.645 +     let pulls = read("pulls/index.html");646 +     assert_eq!(647 +-        pulls.matches("<tr><td class=\"num").count(),648 ++        pulls.matches("<tr><td class=\"title").count(),649 +         1,650 +         "prs match file-count"651 +     );652 +     assert!(653 +         pulls.contains(&format!(654 +-            "<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\">"655 ++            "<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>"656 +         )),657 +         "pr #1 -> uuid"658 +     );659 +diff --git a/tests/releases_api.rs b/tests/releases_api.rs660 +index 0c7e472..04e6cee 100644661 +--- a/tests/releases_api.rs662 ++++ b/tests/releases_api.rs663 +@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {664 + 665 +     // --- api counts == html counts (same struct pass) ---666 +     let issues_html = read("issues/index.html")667 +-        .matches("<tr><td class=\"num")668 ++        .matches("<tr><td class=\"title")669 +         .count();670 +     let pulls_html = read("pulls/index.html")671 +-        .matches("<tr><td class=\"num")672 ++        .matches("<tr><td class=\"title")673 ++        .count();674 ++    let commits_html = read("commits.html")675 ++        .matches("<tr><td class=\"title")676 +         .count();677 +-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();678 +     let releases_html = read("releases/index.html").matches("<tr><td").count();679 +     let wiki_html = read("wiki/index.html")680 +         .matches("<li><a href=\"/wiki/")681 +diff --git a/tests/truncate.rs b/tests/truncate.rs682 +index 12d9004..2f08e2d 100644683 +--- a/tests/truncate.rs684 ++++ b/tests/truncate.rs685 +@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {686 + }687 + 688 + fn assert_every_td_truncated(html: &str, page: &str) {689 ++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real690 ++    // table — trunc + title= kept, no data-label card treatment.691 ++    assert_every_td_truncated_opts(html, page, page != "index.html");692 ++}693 ++694 ++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {695 +     let parts: Vec<&str> = html.split("<td").skip(1).collect();696 +     assert!(!parts.is_empty(), "{page} has cells");697 +     for (i, p) in parts.iter().enumerate() {698 +@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {699 +             head.contains("title="),700 +             "{page} td #{i} carries title= (got {head:?})"701 +         );702 ++        if expect_labels {703 ++            assert!(704 ++                head.contains("data-label="),705 ++                "{page} td #{i} keeps data-label (got {head:?})"706 ++            );707 ++        }708 ++    }709 ++    if !expect_labels {710 +         assert!(711 +-            head.contains("data-label="),712 +-            "{page} td #{i} keeps data-label (got {head:?})"713 ++            !html.contains("data-label="),714 ++            "{page} has no card data-labels"715 +         );716 +     }717 + }718 +-- 719 +2.55.0720 +721 diff --git a/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md722 new file mode 100644723 index 0000000..b1fc288724 --- /dev/null725 +++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md726 @@ -0,0 +1,12 @@727 +---728 +id: 0a15e49a-092a-4c82-a56c-fc03a36d0dae729 +title: m6/cards2730 +state: closed731 +base: master732 +head: m6/cards2733 +draft: false734 +issues: []735 +ci: null736 +created: 2026-09-10T08:57:21.717122363Z737 +updated: 2026-09-10T09:01:43.234179940Z738 +---739 diff --git a/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff740 new file mode 100644741 index 0000000..99535be742 --- /dev/null743 +++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff744 @@ -0,0 +1,421 @@745 +From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 2001746 +From: gl0w <gl0w@world>747 +Date: Thu, 10 Sep 2026 09:57:21 +0100748 +Subject: [PATCH] m6/cards2: file-table exempt, title-first cards749 +750 +---751 + src/build.rs           | 57 ++++++++++++++++++++++++++----------------752 + src/commits.rs         | 12 ++++++---753 + src/forge.rs           | 47 +++++++++++++++++++++++++++++-----754 + src/releases.rs        |  8 ++++--755 + static/style.css       | 37 +++++++++++++++------------756 + tests/forge_readers.rs | 10 ++++----757 + tests/releases_api.rs  |  8 +++---758 + tests/truncate.rs      | 18 +++++++++++--759 + 8 files changed, 137 insertions(+), 60 deletions(-)760 +761 +diff --git a/src/build.rs b/src/build.rs762 +index 6a12e44..f1c0795 100644763 +--- a/src/build.rs764 ++++ b/src/build.rs765 +@@ -579,13 +579,13 @@ pub fn file_table_html(766 +     history: &std::collections::BTreeMap<String, (String, String)>,767 + ) -> String {768 +     let mut out = String::from(769 +-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\770 ++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\771 +          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",772 +     );773 +     if let Some((href, label)) = parent {774 +         out.push_str(&format!(775 +-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\776 +-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",777 ++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\778 ++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",779 +             esc(label),780 +             esc(href),781 +             esc(label)782 +@@ -601,8 +601,8 @@ pub fn file_table_html(783 +         let msg_short = truncate_msg(&msg);784 +         if e.is_dir {785 +             out.push_str(&format!(786 +-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\787 +-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",788 ++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\789 ++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",790 +                 esc(&e.name),791 +                 href_esc(&full),792 +                 esc(&e.name),793 +@@ -613,8 +613,8 @@ pub fn file_table_html(794 +             ));795 +         } else {796 +             out.push_str(&format!(797 +-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\798 +-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",799 ++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\800 ++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",801 +                 esc(&e.name),802 +                 href_esc(&full),803 +                 esc(&e.name),804 +@@ -1732,28 +1732,41 @@ mod tests {805 +         assert!(sub.contains("title=\""), "title attrs kept");806 +     }807 + 808 +-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.809 ++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —810 ++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.811 +     #[test]812 +-    fn file_table_cells_have_data_labels() {813 ++    fn file_table_stays_table_no_card_labels() {814 +         let css = include_str!("../static/style.css");815 +-        assert!(css.contains("content:attr(data-label)"), "label rule");816 +         assert!(css.contains("@media (max-width:767px)"), "narrow query");817 ++        assert!(818 ++            css.contains(".table-scroll:not(.file-scroll)"),819 ++            "cards scoped off file-scroll"820 ++        );821 ++        assert!(822 ++            css.contains(".file-scroll{overflow-x:auto}"),823 ++            "file-scroll keeps h-scroll on narrow"824 ++        );825 +         let (_repo, out) = build_fixture();826 +         for page in ["index.html", "files/sub.html"] {827 +             let html = std::fs::read_to_string(out.join(page)).expect(page);828 +-            let tds = html.matches("<td").count();829 +-            assert!(tds > 0, "{page} has cells");830 +-            assert_eq!(831 +-                html.matches("data-label=").count(),832 +-                tds,833 +-                "{page}: every td labeled"834 ++            assert!(835 ++                html.contains(836 ++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"837 ++                ),838 ++                "{page}: scroll wrapper kept"839 ++            );840 ++            assert!(!html.contains("data-label="), "{page}: no card labels");841 ++            assert!(842 ++                html.contains("<td class=\"title trunc trunc-message\""),843 ++                "{page}: message cell trunc kept"844 ++            );845 ++            assert!(html.contains("title=\""), "{page}: title attrs kept");846 ++            assert!(847 ++                html.contains("<th>Name</th>")848 ++                    && html.contains("<th>Last commit</th>")849 ++                    && html.contains("<th>Age</th>"),850 ++                "{page}: headers kept"851 +             );852 +-            for label in ["Name", "Last commit", "Age"] {853 +-                assert!(854 +-                    html.contains(&format!("data-label=\"{label}\"")),855 +-                    "{page}: label {label}"856 +-                );857 +-            }858 +         }859 +     }860 + 861 +diff --git a/src/commits.rs b/src/commits.rs862 +index 2324ee1..e3aa5de 100644863 +--- a/src/commits.rs864 ++++ b/src/commits.rs865 +@@ -40,15 +40,15 @@ pub fn render_loaded(866 +     if log.is_empty() {867 +         body.push_str("<p class=\"muted\">No commits yet.</p>\n");868 +     } else {869 +-        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");870 ++        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");871 +         for c in log {872 +             body.push_str(&format!(873 +-                "<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",874 ++                "<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",875 ++                esc(&c.subject),876 ++                esc(&c.subject),877 +                 esc(&c.sha),878 +                 esc(&c.sha),879 +                 esc(short_sha(&c.sha)),880 +-                esc(&c.subject),881 +-                esc(&c.subject),882 +                 esc(&c.author),883 +                 esc(&c.author),884 +                 esc(&c.date),885 +@@ -423,5 +423,9 @@ mod tests {886 +                 "label {label}"887 +             );888 +         }889 ++        // m6/cards2 (f0rge #47): card DOM order is subject-first.890 ++        let subj = list.find("data-label=\"subject\"").expect("subject cell");891 ++        let sha = list.find("data-label=\"sha\"").expect("sha cell");892 ++        assert!(subj < sha, "card DOM: subject precedes sha");893 +     }894 + }895 +diff --git a/src/forge.rs b/src/forge.rs896 +index 40aa052..d2827b6 100644897 +--- a/src/forge.rs898 ++++ b/src/forge.rs899 +@@ -998,10 +998,10 @@ fn render_issues_index(900 +     if data.issues.is_empty() {901 +         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");902 +     } else {903 +-        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");904 ++        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");905 +         for issue in &data.issues {906 +             body.push_str(&format!(907 +-                "<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",908 ++                "<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",909 +                 n = issue.number,910 +                 id = esc(&issue.id),911 +                 title = esc(&issue.title),912 +@@ -1059,14 +1059,14 @@ fn render_pulls_index(913 +     if prs.is_empty() {914 +         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");915 +     } else {916 +-        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");917 ++        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");918 +         for pr in prs {919 +             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));920 +             let lanes_plain = format!("{}→{}", pr.head, pr.base);921 +             let draft = if pr.draft { " draft" } else { "" };922 +             let full_title = format!("{}{}", pr.title, draft);923 +             body.push_str(&format!(924 +-                "<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",925 ++                "<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",926 +                 n = pr.number,927 +                 id = esc(&pr.id),928 +                 title = esc(&pr.title),929 +@@ -1823,11 +1823,19 @@ mod tests {930 +             css.contains("content:attr(data-label)"),931 +             "label via ::before"932 +         );933 +-        assert!(css.contains(".table-scroll td::before"), "label rule");934 +         assert!(935 +-            css.contains(".table-scroll td.title::before"),936 ++            css.contains(".table-scroll:not(.file-scroll) td::before"),937 ++            "label rule"938 ++        );939 ++        assert!(940 ++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),941 +             "title leads card"942 +         );943 ++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.944 ++        assert!(945 ++            css.contains(".file-scroll{overflow-x:auto}"),946 ++            "file table keeps h-scroll on narrow"947 ++        );948 +         assert!(949 +             css.contains("display:block") && css.contains("display:flex"),950 +             "card layout"951 +@@ -1894,8 +1902,35 @@ mod tests {952 +                 );953 +             }954 +         }955 ++        // m6/cards2 (f0rge #47): card DOM order is title-first via template956 ++        // cell order (no CSS order hacks) — desktop column headers match.957 ++        for (page, first, second) in [958 ++            (959 ++                "issues/index.html",960 ++                "data-label=\"title\"",961 ++                "data-label=\"#\"",962 ++            ),963 ++            (964 ++                "pulls/index.html",965 ++                "data-label=\"title\"",966 ++                "data-label=\"#\"",967 ++            ),968 ++            (969 ++                "ci/index.html",970 ++                "data-label=\"step\"",971 ++                "data-label=\"exit\"",972 ++            ),973 ++        ] {974 ++            let html = std::fs::read_to_string(out.join(page)).expect(page);975 ++            let a = html.find(first).expect("first cell");976 ++            let b = html.find(second).expect("second cell");977 ++            assert!(a < b, "{page}: card DOM title precedes meta");978 ++        }979 +         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");980 +         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");981 ++        let title_pos = tasks.find("Card task").expect("task title");982 ++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");983 ++        assert!(title_pos < meta_pos, "tasks: title precedes meta");984 +         assert!(tasks.contains("todo (1)"), "tasks group kept");985 +         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");986 +         assert!(987 +diff --git a/src/releases.rs b/src/releases.rs988 +index 68c050b..cceb905 100644989 +--- a/src/releases.rs990 ++++ b/src/releases.rs991 +@@ -395,10 +395,10 @@ pub fn render_loaded(992 +     if releases.is_empty() {993 +         body.push_str("<p class=\"muted\">No releases yet.</p>\n");994 +     } else {995 +-        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");996 ++        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");997 +         for rel in releases {998 +             body.push_str(&format!(999 +-                "<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",1000 ++                "<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",1001 +                 v = esc(&rel.version),1002 +                 ve = esc(&rel.version),1003 +                 t = esc(rel.display_title()),1004 +@@ -584,5 +584,9 @@ mod tests {1005 +                 "label {label}"1006 +             );1007 +         }1008 ++        // m6/cards2 (f0rge #47): card DOM order is title-first.1009 ++        let title = index.find("data-label=\"title\"").expect("title cell");1010 ++        let ver = index.find("data-label=\"version\"").expect("version cell");1011 ++        assert!(title < ver, "card DOM: title precedes version");1012 +     }1013 + }1014 +diff --git a/static/style.css b/static/style.css1015 +index d645df0..a305df3 1006441016 +--- a/static/style.css1017 ++++ b/static/style.css1018 +@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}1019 +    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.1020 +    Narrow: no scroll (overflow visible), thead clipped for SR only, each1021 +    tr becomes a bordered card, each td a labeled flex row via data-label.1022 +-   Title cells lead bold without a repeated label; nothing is deleted. */1023 ++   Title cells lead bold without a repeated label; nothing is deleted.1024 ++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a1025 ++   real table at every width — .file-scroll keeps horizontal scroll and1026 ++   table layout on narrow while every other list table becomes cards. */1027 + @media (max-width:767px){1028 +-  .table-scroll{overflow-x:visible}1029 +-  .table-scroll table,.file-table table{width:100%;min-width:0}1030 +-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1031 +-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}1032 +-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1033 +-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}1034 +-  .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}1035 +-  .table-scroll td:last-child{border-bottom:none}1036 +-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}1037 +-  .table-scroll td.title::before{display:none}1038 +-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}1039 +-  .table-scroll td.num,.table-scroll td.sha{width:auto}1040 +-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1041 +-  .table-scroll tbody tr:hover td{background:none}1042 +-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1043 ++  .table-scroll:not(.file-scroll){overflow-x:visible}1044 ++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}1045 ++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1046 ++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}1047 ++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1048 ++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}1049 ++  .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}1050 ++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}1051 ++  .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}1052 ++  .table-scroll:not(.file-scroll) td.title::before{display:none}1053 ++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}1054 ++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}1055 ++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1056 ++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}1057 ++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1058 ++  .file-scroll{overflow-x:auto}1059 ++  .file-scroll table{width:max-content;min-width:100%}1060 + }1061 +diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs1062 +index c03348b..b43bfcf 1006441063 +--- a/tests/forge_readers.rs1064 ++++ b/tests/forge_readers.rs1065 +@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1066 + 1067 +     // --- issues index: 2 rendered rows (broken one skipped) ---1068 +     let issues = read("issues/index.html");1069 +-    let rows = issues.matches("<tr><td class=\"num").count();1070 ++    let rows = issues.matches("<tr><td class=\"title").count();1071 +     assert_eq!(rows, 2, "rendered issues match file-count minus broken");1072 +     assert!(!issues.contains("No title here"), "broken issue skipped");1073 +     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.1074 +     assert!(1075 +         issues.contains(&format!(1076 +-            "<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\">"1077 ++            "<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>"1078 +         )),1079 +         "issue #1 -> older uuid"1080 +     );1081 +     assert!(1082 +         issues.contains(&format!(1083 +-            "<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\">"1084 ++            "<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>"1085 +         )),1086 +         "issue #2 -> newer uuid"1087 +     );1088 +@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1089 +     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.1090 +     let pulls = read("pulls/index.html");1091 +     assert_eq!(1092 +-        pulls.matches("<tr><td class=\"num").count(),1093 ++        pulls.matches("<tr><td class=\"title").count(),1094 +         1,1095 +         "prs match file-count"1096 +     );1097 +     assert!(1098 +         pulls.contains(&format!(1099 +-            "<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\">"1100 ++            "<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>"1101 +         )),1102 +         "pr #1 -> uuid"1103 +     );1104 +diff --git a/tests/releases_api.rs b/tests/releases_api.rs1105 +index 0c7e472..04e6cee 1006441106 +--- a/tests/releases_api.rs1107 ++++ b/tests/releases_api.rs1108 +@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {1109 + 1110 +     // --- api counts == html counts (same struct pass) ---1111 +     let issues_html = read("issues/index.html")1112 +-        .matches("<tr><td class=\"num")1113 ++        .matches("<tr><td class=\"title")1114 +         .count();1115 +     let pulls_html = read("pulls/index.html")1116 +-        .matches("<tr><td class=\"num")1117 ++        .matches("<tr><td class=\"title")1118 ++        .count();1119 ++    let commits_html = read("commits.html")1120 ++        .matches("<tr><td class=\"title")1121 +         .count();1122 +-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();1123 +     let releases_html = read("releases/index.html").matches("<tr><td").count();1124 +     let wiki_html = read("wiki/index.html")1125 +         .matches("<li><a href=\"/wiki/")1126 +diff --git a/tests/truncate.rs b/tests/truncate.rs1127 +index 12d9004..2f08e2d 1006441128 +--- a/tests/truncate.rs1129 ++++ b/tests/truncate.rs1130 +@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {1131 + }1132 + 1133 + fn assert_every_td_truncated(html: &str, page: &str) {1134 ++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real1135 ++    // table — trunc + title= kept, no data-label card treatment.1136 ++    assert_every_td_truncated_opts(html, page, page != "index.html");1137 ++}1138 ++1139 ++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {1140 +     let parts: Vec<&str> = html.split("<td").skip(1).collect();1141 +     assert!(!parts.is_empty(), "{page} has cells");1142 +     for (i, p) in parts.iter().enumerate() {1143 +@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {1144 +             head.contains("title="),1145 +             "{page} td #{i} carries title= (got {head:?})"1146 +         );1147 ++        if expect_labels {1148 ++            assert!(1149 ++                head.contains("data-label="),1150 ++                "{page} td #{i} keeps data-label (got {head:?})"1151 ++            );1152 ++        }1153 ++    }1154 ++    if !expect_labels {1155 +         assert!(1156 +-            head.contains("data-label="),1157 +-            "{page} td #{i} keeps data-label (got {head:?})"1158 ++            !html.contains("data-label="),1159 ++            "{page} has no card data-labels"1160 +         );1161 +     }1162 + }1163 +-- 1164 +2.55.01165 +1166 diff --git a/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md1167 new file mode 1006441168 index 0000000..52896bf1169 --- /dev/null1170 +++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md1171 @@ -0,0 +1,12 @@1172 +---1173 +id: 1329e344-0a57-4508-aecc-aca2914005be1174 +title: m6/cards21175 +state: open1176 +base: master1177 +head: m6/cards21178 +draft: false1179 +issues: []1180 +ci: null1181 +created: 2026-09-10T09:01:43.245267981Z1182 +updated: null1183 +---1184 diff --git a/src/build.rs b/src/build.rs1185 index 6a12e44..f1c0795 1006441186 --- a/src/build.rs1187 +++ b/src/build.rs1188 @@ -579,13 +579,13 @@ pub fn file_table_html(1189      history: &std::collections::BTreeMap<String, (String, String)>,1190  ) -> String {1191      let mut out = String::from(1192 -        "<div class=\"table-scroll\"><table class=\"file-table\">\n\1193 +        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\1194           <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",1195      );1196      if let Some((href, label)) = parent {1197          out.push_str(&format!(1198 -            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\1199 -             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",1200 +            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\1201 +             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",1202              esc(label),1203              esc(href),1204              esc(label)1205 @@ -601,8 +601,8 @@ pub fn file_table_html(1206          let msg_short = truncate_msg(&msg);1207          if e.is_dir {1208              out.push_str(&format!(1209 -                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\1210 -                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",1211 +                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\1212 +                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",1213                  esc(&e.name),1214                  href_esc(&full),1215                  esc(&e.name),1216 @@ -613,8 +613,8 @@ pub fn file_table_html(1217              ));1218          } else {1219              out.push_str(&format!(1220 -                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\1221 -                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",1222 +                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\1223 +                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",1224                  esc(&e.name),1225                  href_esc(&full),1226                  esc(&e.name),1227 @@ -1732,28 +1732,41 @@ mod tests {1228          assert!(sub.contains("title=\""), "title attrs kept");1229      }1230  1231 -    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.1232 +    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —1233 +    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.1234      #[test]1235 -    fn file_table_cells_have_data_labels() {1236 +    fn file_table_stays_table_no_card_labels() {1237          let css = include_str!("../static/style.css");1238 -        assert!(css.contains("content:attr(data-label)"), "label rule");1239          assert!(css.contains("@media (max-width:767px)"), "narrow query");1240 +        assert!(1241 +            css.contains(".table-scroll:not(.file-scroll)"),1242 +            "cards scoped off file-scroll"1243 +        );1244 +        assert!(1245 +            css.contains(".file-scroll{overflow-x:auto}"),1246 +            "file-scroll keeps h-scroll on narrow"1247 +        );1248          let (_repo, out) = build_fixture();1249          for page in ["index.html", "files/sub.html"] {1250              let html = std::fs::read_to_string(out.join(page)).expect(page);1251 -            let tds = html.matches("<td").count();1252 -            assert!(tds > 0, "{page} has cells");1253 -            assert_eq!(1254 -                html.matches("data-label=").count(),1255 -                tds,1256 -                "{page}: every td labeled"1257 +            assert!(1258 +                html.contains(1259 +                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"1260 +                ),1261 +                "{page}: scroll wrapper kept"1262 +            );1263 +            assert!(!html.contains("data-label="), "{page}: no card labels");1264 +            assert!(1265 +                html.contains("<td class=\"title trunc trunc-message\""),1266 +                "{page}: message cell trunc kept"1267 +            );1268 +            assert!(html.contains("title=\""), "{page}: title attrs kept");1269 +            assert!(1270 +                html.contains("<th>Name</th>")1271 +                    && html.contains("<th>Last commit</th>")1272 +                    && html.contains("<th>Age</th>"),1273 +                "{page}: headers kept"1274              );1275 -            for label in ["Name", "Last commit", "Age"] {1276 -                assert!(1277 -                    html.contains(&format!("data-label=\"{label}\"")),1278 -                    "{page}: label {label}"1279 -                );1280 -            }1281          }1282      }1283  1284 diff --git a/src/commits.rs b/src/commits.rs1285 index 2324ee1..e3aa5de 1006441286 --- a/src/commits.rs1287 +++ b/src/commits.rs1288 @@ -40,15 +40,15 @@ pub fn render_loaded(1289      if log.is_empty() {1290          body.push_str("<p class=\"muted\">No commits yet.</p>\n");1291      } else {1292 -        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");1293 +        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");1294          for c in log {1295              body.push_str(&format!(1296 -                "<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",1297 +                "<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",1298 +                esc(&c.subject),1299 +                esc(&c.subject),1300                  esc(&c.sha),1301                  esc(&c.sha),1302                  esc(short_sha(&c.sha)),1303 -                esc(&c.subject),1304 -                esc(&c.subject),1305                  esc(&c.author),1306                  esc(&c.author),1307                  esc(&c.date),1308 @@ -423,5 +423,9 @@ mod tests {1309                  "label {label}"1310              );1311          }1312 +        // m6/cards2 (f0rge #47): card DOM order is subject-first.1313 +        let subj = list.find("data-label=\"subject\"").expect("subject cell");1314 +        let sha = list.find("data-label=\"sha\"").expect("sha cell");1315 +        assert!(subj < sha, "card DOM: subject precedes sha");1316      }1317  }1318 diff --git a/src/forge.rs b/src/forge.rs1319 index f92aabd..01107e8 1006441320 --- a/src/forge.rs1321 +++ b/src/forge.rs1322 @@ -1052,12 +1052,13 @@ fn render_issues_index(1323      if data.issues.is_empty() {1324          body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");1325      } else {1326 -        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");1327 +        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");1328          // m6/sort (#48 + #49): open first (newest-first within), then1329          // non-open (newest-first within). Numbers stay creation-rank.1330 +        // m6/cards2 (#46 + #47): title-first column order.1331          for issue in data.issues_in_display_order() {1332              body.push_str(&format!(1333 -                "<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",1334 +                "<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",1335                  n = issue.number,1336                  id = esc(&issue.id),1337                  title = esc(&issue.title),1338 @@ -1114,17 +1115,18 @@ fn render_pulls_index(1339      if data.prs.is_empty() {1340          body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");1341      } else {1342 -        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");1343 +        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");1344          // m6/sort (#48 + #49): open (incl drafts) first (newest-first1345          // within), then closed/merged (newest-first within).1346          // Numbers stay creation-rank.1347 +        // m6/cards2 (#46 + #47): title-first column order.1348          for pr in data.prs_in_display_order() {1349              let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));1350              let lanes_plain = format!("{}→{}", pr.head, pr.base);1351              let draft = if pr.draft { " draft" } else { "" };1352              let full_title = format!("{}{}", pr.title, draft);1353              body.push_str(&format!(1354 -                "<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",1355 +                "<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",1356                  n = pr.number,1357                  id = esc(&pr.id),1358                  title = esc(&pr.title),1359 @@ -2073,11 +2075,19 @@ mod tests {1360              css.contains("content:attr(data-label)"),1361              "label via ::before"1362          );1363 -        assert!(css.contains(".table-scroll td::before"), "label rule");1364          assert!(1365 -            css.contains(".table-scroll td.title::before"),1366 +            css.contains(".table-scroll:not(.file-scroll) td::before"),1367 +            "label rule"1368 +        );1369 +        assert!(1370 +            css.contains(".table-scroll:not(.file-scroll) td.title::before"),1371              "title leads card"1372          );1373 +        // m6/cards2 (f0rge #46): file table exempt from the card reflow.1374 +        assert!(1375 +            css.contains(".file-scroll{overflow-x:auto}"),1376 +            "file table keeps h-scroll on narrow"1377 +        );1378          assert!(1379              css.contains("display:block") && css.contains("display:flex"),1380              "card layout"1381 @@ -2144,8 +2154,35 @@ mod tests {1382                  );1383              }1384          }1385 +        // m6/cards2 (f0rge #47): card DOM order is title-first via template1386 +        // cell order (no CSS order hacks) — desktop column headers match.1387 +        for (page, first, second) in [1388 +            (1389 +                "issues/index.html",1390 +                "data-label=\"title\"",1391 +                "data-label=\"#\"",1392 +            ),1393 +            (1394 +                "pulls/index.html",1395 +                "data-label=\"title\"",1396 +                "data-label=\"#\"",1397 +            ),1398 +            (1399 +                "ci/index.html",1400 +                "data-label=\"step\"",1401 +                "data-label=\"exit\"",1402 +            ),1403 +        ] {1404 +            let html = std::fs::read_to_string(out.join(page)).expect(page);1405 +            let a = html.find(first).expect("first cell");1406 +            let b = html.find(second).expect("second cell");1407 +            assert!(a < b, "{page}: card DOM title precedes meta");1408 +        }1409          let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");1410          assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");1411 +        let title_pos = tasks.find("Card task").expect("task title");1412 +        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");1413 +        assert!(title_pos < meta_pos, "tasks: title precedes meta");1414          assert!(tasks.contains("todo (1)"), "tasks group kept");1415          let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");1416          assert!(1417 diff --git a/src/releases.rs b/src/releases.rs1418 index 68c050b..cceb905 1006441419 --- a/src/releases.rs1420 +++ b/src/releases.rs1421 @@ -395,10 +395,10 @@ pub fn render_loaded(1422      if releases.is_empty() {1423          body.push_str("<p class=\"muted\">No releases yet.</p>\n");1424      } else {1425 -        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");1426 +        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");1427          for rel in releases {1428              body.push_str(&format!(1429 -                "<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",1430 +                "<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",1431                  v = esc(&rel.version),1432                  ve = esc(&rel.version),1433                  t = esc(rel.display_title()),1434 @@ -584,5 +584,9 @@ mod tests {1435                  "label {label}"1436              );1437          }1438 +        // m6/cards2 (f0rge #47): card DOM order is title-first.1439 +        let title = index.find("data-label=\"title\"").expect("title cell");1440 +        let ver = index.find("data-label=\"version\"").expect("version cell");1441 +        assert!(title < ver, "card DOM: title precedes version");1442      }1443  }1444 diff --git a/static/style.css b/static/style.css1445 index d645df0..a305df3 1006441446 --- a/static/style.css1447 +++ b/static/style.css1448 @@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}1449     Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.1450     Narrow: no scroll (overflow visible), thead clipped for SR only, each1451     tr becomes a bordered card, each td a labeled flex row via data-label.1452 -   Title cells lead bold without a repeated label; nothing is deleted. */1453 +   Title cells lead bold without a repeated label; nothing is deleted.1454 +   File table exempt (m6/cards2, f0rge #46): dense reference data stays a1455 +   real table at every width — .file-scroll keeps horizontal scroll and1456 +   table layout on narrow while every other list table becomes cards. */1457  @media (max-width:767px){1458 -  .table-scroll{overflow-x:visible}1459 -  .table-scroll table,.file-table table{width:100%;min-width:0}1460 -  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1461 -  .table-scroll tbody,.table-scroll tr{display:block;width:auto}1462 -  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1463 -  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}1464 -  .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}1465 -  .table-scroll td:last-child{border-bottom:none}1466 -  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}1467 -  .table-scroll td.title::before{display:none}1468 -  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}1469 -  .table-scroll td.num,.table-scroll td.sha{width:auto}1470 -  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1471 -  .table-scroll tbody tr:hover td{background:none}1472 -  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1473 +  .table-scroll:not(.file-scroll){overflow-x:visible}1474 +  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}1475 +  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1476 +  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}1477 +  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1478 +  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}1479 +  .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}1480 +  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}1481 +  .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}1482 +  .table-scroll:not(.file-scroll) td.title::before{display:none}1483 +  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}1484 +  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}1485 +  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1486 +  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}1487 +  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1488 +  .file-scroll{overflow-x:auto}1489 +  .file-scroll table{width:max-content;min-width:100%}1490  }1491 diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs1492 index c03348b..b43bfcf 1006441493 --- a/tests/forge_readers.rs1494 +++ b/tests/forge_readers.rs1495 @@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1496  1497      // --- issues index: 2 rendered rows (broken one skipped) ---1498      let issues = read("issues/index.html");1499 -    let rows = issues.matches("<tr><td class=\"num").count();1500 +    let rows = issues.matches("<tr><td class=\"title").count();1501      assert_eq!(rows, 2, "rendered issues match file-count minus broken");1502      assert!(!issues.contains("No title here"), "broken issue skipped");1503      // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.1504      assert!(1505          issues.contains(&format!(1506 -            "<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\">"1507 +            "<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>"1508          )),1509          "issue #1 -> older uuid"1510      );1511      assert!(1512          issues.contains(&format!(1513 -            "<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\">"1514 +            "<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>"1515          )),1516          "issue #2 -> newer uuid"1517      );1518 @@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1519      // (4): pulls — same shape + head/base/draft/ci + patch <pre>.1520      let pulls = read("pulls/index.html");1521      assert_eq!(1522 -        pulls.matches("<tr><td class=\"num").count(),1523 +        pulls.matches("<tr><td class=\"title").count(),1524          1,1525          "prs match file-count"1526      );1527      assert!(1528          pulls.contains(&format!(1529 -            "<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\">"1530 +            "<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>"1531          )),1532          "pr #1 -> uuid"1533      );1534 diff --git a/tests/releases_api.rs b/tests/releases_api.rs1535 index 0c7e472..04e6cee 1006441536 --- a/tests/releases_api.rs1537 +++ b/tests/releases_api.rs1538 @@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {1539  1540      // --- api counts == html counts (same struct pass) ---1541      let issues_html = read("issues/index.html")1542 -        .matches("<tr><td class=\"num")1543 +        .matches("<tr><td class=\"title")1544          .count();1545      let pulls_html = read("pulls/index.html")1546 -        .matches("<tr><td class=\"num")1547 +        .matches("<tr><td class=\"title")1548 +        .count();1549 +    let commits_html = read("commits.html")1550 +        .matches("<tr><td class=\"title")1551          .count();1552 -    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();1553      let releases_html = read("releases/index.html").matches("<tr><td").count();1554      let wiki_html = read("wiki/index.html")1555          .matches("<li><a href=\"/wiki/")1556 diff --git a/tests/truncate.rs b/tests/truncate.rs1557 index 12d9004..2f08e2d 1006441558 --- a/tests/truncate.rs1559 +++ b/tests/truncate.rs1560 @@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {1561  }1562  1563  fn assert_every_td_truncated(html: &str, page: &str) {1564 +    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real1565 +    // table — trunc + title= kept, no data-label card treatment.1566 +    assert_every_td_truncated_opts(html, page, page != "index.html");1567 +}1568 +1569 +fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {1570      let parts: Vec<&str> = html.split("<td").skip(1).collect();1571      assert!(!parts.is_empty(), "{page} has cells");1572      for (i, p) in parts.iter().enumerate() {1573 @@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {1574              head.contains("title="),1575              "{page} td #{i} carries title= (got {head:?})"1576          );1577 +        if expect_labels {1578 +            assert!(1579 +                head.contains("data-label="),1580 +                "{page} td #{i} keeps data-label (got {head:?})"1581 +            );1582 +        }1583 +    }1584 +    if !expect_labels {1585          assert!(1586 -            head.contains("data-label="),1587 -            "{page} td #{i} keeps data-label (got {head:?})"1588 +            !html.contains("data-label="),1589 +            "{page} has no card data-labels"1590          );1591      }1592  }1593 -- 1594 2.55.01595