From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 2001 From: gl0w Date: Thu, 10 Sep 2026 09:57:21 +0100 Subject: [PATCH] m6/cards2: file-table exempt, title-first cards --- src/build.rs | 57 ++++++++++++++++++++++++++---------------- src/commits.rs | 12 ++++++--- src/forge.rs | 47 +++++++++++++++++++++++++++++----- src/releases.rs | 8 ++++-- static/style.css | 37 +++++++++++++++------------ tests/forge_readers.rs | 10 ++++---- tests/releases_api.rs | 8 +++--- tests/truncate.rs | 18 +++++++++++-- 8 files changed, 137 insertions(+), 60 deletions(-) diff --git a/src/build.rs b/src/build.rs index 6a12e44..f1c0795 100644 --- a/src/build.rs +++ b/src/build.rs @@ -579,13 +579,13 @@ pub fn file_table_html( history: &std::collections::BTreeMap, ) -> String { let mut out = String::from( - "
\n\ + "
\n\ \n\n", ); if let Some((href, label)) = parent { out.push_str(&format!( - "\ - \n", + "\ + \n", esc(label), esc(href), esc(label) @@ -601,8 +601,8 @@ pub fn file_table_html( let msg_short = truncate_msg(&msg); if e.is_dir { out.push_str(&format!( - "\ - \n", + "\ + \n", esc(&e.name), href_esc(&full), esc(&e.name), @@ -613,8 +613,8 @@ pub fn file_table_html( )); } else { out.push_str(&format!( - "\ - \n", + "\ + \n", esc(&e.name), href_esc(&full), esc(&e.name), @@ -1732,28 +1732,41 @@ mod tests { assert!(sub.contains("title=\""), "title attrs kept"); } - /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards. + /// m6/cards2 (f0rge #46): file table stays a real table on all widths — + /// no data-label card treatment, scroll wrapper kept, trunc + title kept. #[test] - fn file_table_cells_have_data_labels() { + fn file_table_stays_table_no_card_labels() { let css = include_str!("../static/style.css"); - assert!(css.contains("content:attr(data-label)"), "label rule"); assert!(css.contains("@media (max-width:767px)"), "narrow query"); + assert!( + css.contains(".table-scroll:not(.file-scroll)"), + "cards scoped off file-scroll" + ); + assert!( + css.contains(".file-scroll{overflow-x:auto}"), + "file-scroll keeps h-scroll on narrow" + ); let (_repo, out) = build_fixture(); for page in ["index.html", "files/sub.html"] { let html = std::fs::read_to_string(out.join(page)).expect(page); - let tds = html.matches(" 0, "{page} has cells"); - assert_eq!( - html.matches("data-label=").count(), - tds, - "{page}: every td labeled" + assert!( + html.contains( + "
NameLast commitAge
{}
{}
{}/{}{}
{}/{}{}
{}{}{}
{}{}{}
" + ), + "{page}: scroll wrapper kept" + ); + assert!(!html.contains("data-label="), "{page}: no card labels"); + assert!( + html.contains("") + && html.contains(""), + "{page}: headers kept" ); - for label in ["Name", "Last commit", "Age"] { - assert!( - html.contains(&format!("data-label=\"{label}\"")), - "{page}: label {label}" - ); - } } } diff --git a/src/commits.rs b/src/commits.rs index 2324ee1..e3aa5de 100644 --- a/src/commits.rs +++ b/src/commits.rs @@ -40,15 +40,15 @@ pub fn render_loaded( if log.is_empty() { body.push_str("

No commits yet.

\n"); } else { - body.push_str("
Name") + && html.contains("Last commitAge
\n\n\n"); + body.push_str("
shasubjectauthordate
\n\n\n"); for c in log { body.push_str(&format!( - "\n", + "\n", + esc(&c.subject), + esc(&c.subject), esc(&c.sha), esc(&c.sha), esc(short_sha(&c.sha)), - esc(&c.subject), - esc(&c.subject), esc(&c.author), esc(&c.author), esc(&c.date), @@ -423,5 +423,9 @@ mod tests { "label {label}" ); } + // m6/cards2 (f0rge #47): card DOM order is subject-first. + let subj = list.find("data-label=\"subject\"").expect("subject cell"); + let sha = list.find("data-label=\"sha\"").expect("sha cell"); + assert!(subj < sha, "card DOM: subject precedes sha"); } } diff --git a/src/forge.rs b/src/forge.rs index 40aa052..d2827b6 100644 --- a/src/forge.rs +++ b/src/forge.rs @@ -998,10 +998,10 @@ fn render_issues_index( if data.issues.is_empty() { body.push_str("

No issues yet. Suggest one — no account needed.

\n"); } else { - body.push_str("
subjectshaauthordate
{}{}{}{}
{}{}{}{}
\n\n\n"); + body.push_str("
#titlestatecommentscreated
\n\n\n"); for issue in &data.issues { body.push_str(&format!( - "\n", + "\n", n = issue.number, id = esc(&issue.id), title = esc(&issue.title), @@ -1059,14 +1059,14 @@ fn render_pulls_index( if prs.is_empty() { body.push_str("

No pull requests yet. Suggest one — no account needed.

\n"); } else { - body.push_str("
title#statecommentscreated
{n}{title}{state}{comments}{created}
{title}{n}{state}{comments}{created}
\n\n\n"); + body.push_str("
#titlestatehead→basecreated
\n\n\n"); for pr in prs { let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base)); let lanes_plain = format!("{}→{}", pr.head, pr.base); let draft = if pr.draft { " draft" } else { "" }; let full_title = format!("{}{}", pr.title, draft); body.push_str(&format!( - "\n", + "\n", n = pr.number, id = esc(&pr.id), title = esc(&pr.title), @@ -1823,11 +1823,19 @@ mod tests { css.contains("content:attr(data-label)"), "label via ::before" ); - assert!(css.contains(".table-scroll td::before"), "label rule"); assert!( - css.contains(".table-scroll td.title::before"), + css.contains(".table-scroll:not(.file-scroll) td::before"), + "label rule" + ); + assert!( + css.contains(".table-scroll:not(.file-scroll) td.title::before"), "title leads card" ); + // m6/cards2 (f0rge #46): file table exempt from the card reflow. + assert!( + css.contains(".file-scroll{overflow-x:auto}"), + "file table keeps h-scroll on narrow" + ); assert!( css.contains("display:block") && css.contains("display:flex"), "card layout" @@ -1894,8 +1902,35 @@ mod tests { ); } } + // m6/cards2 (f0rge #47): card DOM order is title-first via template + // cell order (no CSS order hacks) — desktop column headers match. + for (page, first, second) in [ + ( + "issues/index.html", + "data-label=\"title\"", + "data-label=\"#\"", + ), + ( + "pulls/index.html", + "data-label=\"title\"", + "data-label=\"#\"", + ), + ( + "ci/index.html", + "data-label=\"step\"", + "data-label=\"exit\"", + ), + ] { + let html = std::fs::read_to_string(out.join(page)).expect(page); + let a = html.find(first).expect("first cell"); + let b = html.find(second).expect("second cell"); + assert!(a < b, "{page}: card DOM title precedes meta"); + } let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks"); assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper"); + let title_pos = tasks.find("Card task").expect("task title"); + let meta_pos = tasks.find("cccccccc").expect("task meta"); + assert!(title_pos < meta_pos, "tasks: title precedes meta"); assert!(tasks.contains("todo (1)"), "tasks group kept"); let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues"); assert!( diff --git a/src/releases.rs b/src/releases.rs index 68c050b..cceb905 100644 --- a/src/releases.rs +++ b/src/releases.rs @@ -395,10 +395,10 @@ pub fn render_loaded( if releases.is_empty() { body.push_str("

No releases yet.

\n"); } else { - body.push_str("
title#statehead→basecreated
{n}{title}{draft}{state}{lanes}{created}
{title}{draft}{n}{state}{lanes}{created}
\n\n\n"); + body.push_str("
versiontitlecreatedartifacts
\n\n\n"); for rel in releases { body.push_str(&format!( - "\n", + "\n", v = esc(&rel.version), ve = esc(&rel.version), t = esc(rel.display_title()), @@ -584,5 +584,9 @@ mod tests { "label {label}" ); } + // m6/cards2 (f0rge #47): card DOM order is title-first. + let title = index.find("data-label=\"title\"").expect("title cell"); + let ver = index.find("data-label=\"version\"").expect("version cell"); + assert!(title < ver, "card DOM: title precedes version"); } } diff --git a/static/style.css b/static/style.css index d645df0..a305df3 100644 --- a/static/style.css +++ b/static/style.css @@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem} Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above. Narrow: no scroll (overflow visible), thead clipped for SR only, each tr becomes a bordered card, each td a labeled flex row via data-label. - Title cells lead bold without a repeated label; nothing is deleted. */ + Title cells lead bold without a repeated label; nothing is deleted. + File table exempt (m6/cards2, f0rge #46): dense reference data stays a + real table at every width — .file-scroll keeps horizontal scroll and + table layout on narrow while every other list table becomes cards. */ @media (max-width:767px){ - .table-scroll{overflow-x:visible} - .table-scroll table,.file-table table{width:100%;min-width:0} - .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap} - .table-scroll tbody,.table-scroll tr{display:block;width:auto} - .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0} - .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal} - .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} - .table-scroll td:last-child{border-bottom:none} - .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere} - .table-scroll td.title::before{display:none} - .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0} - .table-scroll td.num,.table-scroll td.sha{width:auto} - .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0} - .table-scroll tbody tr:hover td{background:none} - .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip} + .table-scroll:not(.file-scroll){overflow-x:visible} + .table-scroll:not(.file-scroll) table{width:100%;min-width:0} + .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap} + .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto} + .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0} + .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal} + .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} + .table-scroll:not(.file-scroll) td:last-child{border-bottom:none} + .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} + .table-scroll:not(.file-scroll) td.title::before{display:none} + .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0} + .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto} + .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0} + .table-scroll:not(.file-scroll) tbody tr:hover td{background:none} + .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip} + .file-scroll{overflow-x:auto} + .file-scroll table{width:max-content;min-width:100%} } diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs index c03348b..b43bfcf 100644 --- a/tests/forge_readers.rs +++ b/tests/forge_readers.rs @@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() { // --- issues index: 2 rendered rows (broken one skipped) --- let issues = read("issues/index.html"); - let rows = issues.matches("" )), "issue #1 -> older uuid" ); assert!( issues.contains(&format!( - "" )), "issue #2 -> newer uuid" ); @@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() { // (4): pulls — same shape + head/base/draft/ci + patch
.
     let pulls = read("pulls/index.html");
     assert_eq!(
-        pulls.matches("
" )), "pr #1 -> uuid" ); diff --git a/tests/releases_api.rs b/tests/releases_api.rs index 0c7e472..04e6cee 100644 --- a/tests/releases_api.rs +++ b/tests/releases_api.rs @@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() { // --- api counts == html counts (same struct pass) --- let issues_html = read("issues/index.html") - .matches("
titleversioncreatedartifacts
{ve}{t}{c}{n}
{t}{ve}{c}{n}
1" + "Older issue12" + "Newer issue2
1" + "Fix the thing1
(PathBuf, String, String) { } fn assert_every_td_truncated(html: &str, page: &str) { + // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real + // table — trunc + title= kept, no data-label card treatment. + assert_every_td_truncated_opts(html, page, page != "index.html"); +} + +fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) { let parts: Vec<&str> = html.split("