m6/finish2: spacing, 2x logo, footer right, clone summary meta
Message
m6/finish2: spacing, 2x logo, footer right, clone summary meta
Diff
src/build.rs+92 −31
--- a/src/build.rs+++ b/src/build.rs@@ -639,7 +639,9 @@ pub fn file_table_html(639639 /// bundle-first as before (`git clone <url>` first when a clone URL is640640 /// configured, else local bundle) plus the bundle/tarball links with641641 /// sizes and shas. The `<summary>` always shows `Clone & download`642-/// plus a branch@sha hint.642+/// plus a branch@sha hint. The summary also carries the CI verdict badge643+/// (m6 finish, f0rge #53): `Clone & download · branch @ sha · CI badge`644+/// so the standalone repo-meta branch/CI line is gone (no duplication).643645 #[must_use]644646 #[allow(clippy::too_many_arguments)]645647 pub fn clone_block(@@ -647,6 +649,7 @@ pub fn clone_block(647649 public_origin: Option<&str>,648650 branch: &str,649651 short_sha: &str,652+ ci_verdict: &str,650653 bundle_size: u64,651654 bundle_sha: Option<&str>,652655 tarball_size: u64,@@ -674,11 +677,13 @@ pub fn clone_block(674677 human_size(tarball_size)675678 ),676679 };680+ let badge = crate::forge::verdict_badge(ci_verdict);677681 let mut out = format!(678682 "<details class=\"clonebox clone-block\" aria-label=\"Clone and download\">\n\679- <summary>Clone & download <span class=\"muted\">{} @ {}</span></summary>\n<div class=\"clone-body\">\n",683+ <summary>Clone & download \u{b7} {} @ {} \u{b7} CI {}</summary>\n<div class=\"clone-body\">\n",680684 esc(branch),681- esc(short_sha)685+ esc(short_sha),686+ badge682687 );683688 if let Some(origin) = public_origin.map(str::trim).filter(|s| !s.is_empty()) {684689 let base = origin.trim_end_matches('/');@@ -1071,28 +1076,21 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result<BuildSu10711076 .map(|m| m.len())10721077 .unwrap_or(0);10731078 1074- // index.html header: stacked repo-head (name + clone + meta) so narrow1075- // screens wrap safely; clone/sha break-all via `.clone`/`.repo-meta`.1076- // No counts line here (m6 open-counts, f0rge #36): nav carries the1077- // open issues/pulls counts; branch/sha/CI meta stay.1079+ // index.html header: stacked repo-head (name + desc + clonebox) so narrow1080+ // screens wrap safely. Branch/sha/CI live ONLY in the clone summary1081+ // (m6 finish, f0rge #53): `Clone & download · branch @ sha · CI badge`1082+ // — the old standalone repo-meta branch/CI line is deleted (no dupe).1083+ // No counts line here (m6 open-counts, f0rge #36): nav carries counts.10781084 let mut body = format!("<div class=\"repo-head\"><h1>{}</h1>\n", esc(&name));10791085 if let Some(d) = &description {10801086 body.push_str(&format!("<p class=\"repo-desc\">{}</p>\n", esc(d)));10811087 }1082- body.push_str(&format!(1083- "<p class=\"repo-meta\">Branch <code>{}</code> @ <code>{}</code></p>\n",1084- esc(&branch),1085- esc(&short_sha)1086- ));1087- body.push_str(&format!(1088- "<p>CI: {}</p>\n",1089- forge::verdict_badge(forge_data.ci.latest_verdict()),1090- ));10911088 body.push_str(&clone_block(10921089 clone_url.as_deref(),10931090 public_origin.as_deref(),10941091 &branch,10951092 &short_sha,1093+ forge_data.ci.latest_verdict(),10961094 bundle_size,10971095 file_hash(&snap.bundle).as_deref(),10981096 tarball_size,@@ -1284,18 +1282,25 @@ mod tests {12841282 "raw HTML escaped"12851283 );12861284 assert!(index.contains("<script>"), "escaped HTML present");1287- // Header: branch + short sha, live open f0rge counts, CI verdict badge.1285+ // Header (m6 finish, f0rge #53): branch/sha/CI live ONLY in the1286+ // clone summary — no standalone repo-meta / CI dupe line.12881287 assert!(index.contains("main"), "branch shown");12891288 assert!(!index.contains("TODO"), "no TODO markers left");12901289 assert!(index.contains("issues (0)"), "nav open issues 0");12911290 assert!(index.contains("pulls (0)"), "nav open pulls 0");1292- assert!(index.contains("repo-meta"), "branch/sha meta kept");1293- assert!(index.contains("CI:"), "CI verdict shown");1294- assert!(index.contains("badge"), "CI verdict badge");1291+ assert!(!index.contains("repo-meta"), "no dupe repo-meta line");1292+ assert!(!index.contains("<p>CI:"), "no dupe CI line");1293+ assert!(index.contains("badge"), "CI verdict badge in summary");1294+ let summ = index.find("<summary>Clone").expect("summary");1295+ let send = index[summ..].find("</summary>").expect("summary end") + summ;1296+ let summary = &index[summ..send];1297+ assert!(summary.contains("main"), "branch in summary");1298+ assert!(summary.contains("CI"), "CI in summary");1299+ assert!(summary.contains("badge"), "badge in summary");12951300 // f0rge #36: code tab carries no redundant counts line.12961301 let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");1297- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;1298- let head = &index[head_start..head_end];1302+ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;1303+ let head = &index[head_start..details_end];12991304 for needle in ["issues (", "pulls (", "tasks (", "releases ("] {13001305 assert!(!head.contains(needle), "code tab drops {needle}");13011306 }@@ -1362,11 +1367,15 @@ mod tests {13621367 let index = std::fs::read_to_string(out.join("index.html")).expect("index");13631368 assert!(index.contains("issues (1)"), "nav open issues");13641369 assert!(index.contains("pulls (2)"), "nav open pulls (draft incl)");1365- assert!(index.contains("repo-meta"), "branch/sha meta kept");1366- assert!(index.contains("CI:"), "CI meta kept");1370+ assert!(1371+ !index.contains("repo-meta"),1372+ "no dupe repo-meta (f0rge #53)"1373+ );1374+ assert!(!index.contains("<p>CI:"), "no dupe CI line (f0rge #53)");1375+ assert!(index.contains("badge"), "CI badge in summary");13671376 let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");1368- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;1369- let head = &index[head_start..head_end];1377+ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;1378+ let head = &index[head_start..details_end];13701379 for needle in ["issues (", "pulls (", "tasks (", "releases ("] {13711380 assert!(!head.contains(needle), "code tab drops {needle}");13721381 }@@ -2041,13 +2050,15 @@ mod tests {20412050 let out = mktemp(&format!("out-ci-{verdict}"));20422051 run_build(&repo, &out, &opts()).expect("build");20432052 let index = std::fs::read_to_string(out.join("index.html")).expect("index");2044- let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");2045- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;2046- let head = &index[head_start..head_end];2053+ let summ = index.find("<summary>Clone").expect("summary");2054+ let send = index[summ..].find("</summary>").expect("summary end") + summ;2055+ let summary = &index[summ..send];20472056 assert!(2048- head.contains(&format!("badge {verdict}")),2049- "code head carries badge {verdict}"2057+ summary.contains(&format!("badge {verdict}")),2058+ "clone summary carries badge {verdict}"20502059 );2060+ assert!(!index.contains("repo-meta"), "no dupe meta");2061+ assert!(!index.contains("<p>CI:"), "no dupe CI line");20512062 }20522063 }20532064 @@ -2249,4 +2260,54 @@ mod tests {22492260 "ignored still in raw"22502261 );22512262 }2263+2264+ #[test]2265+ fn finish_page_heads_and_clone_summary() {2266+ // m6 finish (f0rge #50 + #53): one head pattern everywhere, branch+CI2267+ // only in the clone summary, no dupe line.2268+ let (_repo, out) = build_fixture();2269+ let read = |rel: &str| std::fs::read_to_string(out.join(rel)).expect(rel);2270+ let index = read("index.html");2271+ // Code tab: repo-head with h1, summary carries branch @ sha + CI.2272+ let head_pos = index.find("<div class=\"repo-head\">").expect("repo-head");2273+ assert!(index[head_pos..].contains("<h1>"), "code h1 in repo-head");2274+ let s0 = index.find("<summary>Clone").expect("summary");2275+ let s1 = index[s0..].find("</summary>").expect("summary end") + s0;2276+ let summary = &index[s0..s1];2277+ assert!(summary.contains("Clone & download"), "summary label");2278+ assert!(summary.contains("main"), "branch in summary");2279+ assert!(summary.contains("CI"), "CI in summary");2280+ assert!(summary.contains("badge"), "CI badge in summary");2281+ assert!(!index.contains("repo-meta"), "no dupe repo-meta");2282+ assert!(!index.contains("<p>CI:"), "no dupe CI line");2283+ // Branch appears in summary; outside details it is not repeated as2284+ // a meta line (description/counts carry no branch text here).2285+ let details_pos = index.find("<details class=\"clonebox").expect("details");2286+ assert!(2287+ s0 > head_pos && s0 >= details_pos,2288+ "summary inside code head"2289+ );2290+ // Every index/Form page opens with the same page-head h1 row.2291+ for (rel, h1) in [2292+ ("issues/index.html", "<h1>Issues</h1>"),2293+ ("pulls/index.html", "<h1>Pulls</h1>"),2294+ ("commits.html", "<h1>Commits</h1>"),2295+ ("tasks/index.html", "<h1>Tasks</h1>"),2296+ ("ci/index.html", "<h1>CI</h1>"),2297+ ("releases/index.html", "<h1>Releases</h1>"),2298+ ("wiki/index.html", "<h1>Wiki</h1>"),2299+ ("new-issue.html", "<h1>Suggest an issue</h1>"),2300+ ("new-pr.html", "<h1>Suggest a change</h1>"),2301+ ] {2302+ let html = read(rel);2303+ let hpos = html.find("<div class=\"page-head\">").expect(rel);2304+ let h1pos = html.find(h1).unwrap_or_else(|| panic!("{rel} missing h1"));2305+ let close = html[hpos..].find("</div>").expect("close") + hpos;2306+ assert!(h1pos > hpos && h1pos < close, "{rel}: h1 inside page-head");2307+ }2308+ // Agents guide renders its markdown h1; spacing comes from the2309+ // shared `main>h1:first-child` gap-var rule (CSS assert in layout).2310+ let agents = read("agents.html");2311+ assert!(agents.contains("<h1>"), "agents h1");2312+ }22522313 }
src/commits.rs+1 −1
--- a/src/commits.rs+++ b/src/commits.rs@@ -36,7 +36,7 @@ pub fn render_loaded(3636 snap_sha: &str,3737 description: Option<&str>,3838 ) -> Result<usize, String> {39- let mut body = String::from("<h1>Commits</h1>\n");39+ let mut body = String::from("<div class=\"page-head\"><h1>Commits</h1></div>\n");4040 if log.is_empty() {4141 body.push_str("<p class=\"muted\">No commits yet.</p>\n");4242 } else {
src/forge.rs+2 −2
--- a/src/forge.rs+++ b/src/forge.rs@@ -1161,7 +1161,7 @@ fn render_tasks_index(11611161 short_sha: &str,11621162 description: Option<&str>,11631163 ) -> String {1164- let mut body = String::from("<h1>Tasks</h1>\n");1164+ let mut body = String::from("<div class=\"page-head\"><h1>Tasks</h1></div>\n");11651165 if tasks.is_empty() {11661166 body.push_str("<p class=\"muted\">No tasks yet.</p>\n");11671167 } else {@@ -1213,7 +1213,7 @@ fn render_ci_index(12131213 short_sha: &str,12141214 description: Option<&str>,12151215 ) -> String {1216- let mut body = String::from("<h1>CI</h1>\n");1216+ let mut body = String::from("<div class=\"page-head\"><h1>CI</h1></div>\n");12171217 body.push_str(&meta_row("runner:", &esc(&ci.runner)));12181218 if ci.steps.is_empty() {12191219 body.push_str("<p class=\"muted\">No CI steps configured.</p>\n");
src/inbox.rs+1 −1
--- a/src/inbox.rs+++ b/src/inbox.rs@@ -619,7 +619,7 @@ fn form_page(619619 description: Option<&str>,620620 ) -> String {621621 let body = format!(622- "<h1>{heading}</h1>\n<p>{intro}</p>\n\622+ "<div class=\"page-head\"><h1>{heading}</h1></div>\n<p>{intro}</p>\n\623623 <p>What happens: your text goes to a private inbox only the maintainer reads — \624624 never published on this site. No account needed. The server answers a one-time \625625 ack id and shows nothing back.</p>\n\
src/layout.rs+47 −1
--- a/src/layout.rs+++ b/src/layout.rs@@ -293,7 +293,7 @@ fn page_site_inner(293293 <html lang=\"en\">\n\294294 <head>\n{head}</head>\n\295295 <body>\n\296-<header class=\"site-header\"><div class=\"wrap header-bar\"><a class=\"brand-lockup\" href=\"/\"><img class=\"site-logo\" src=\"/logo.svg\" alt=\"hu13 logo\" width=\"96\" height=\"32\"></a><span class=\"sep\" aria-hidden=\"true\">•</span><nav class=\"site-nav nav-scroll\" aria-label=\"Site\"><div class=\"nav-scroll-row\">{nav_active}</div></nav></div></header>\n\296+<header class=\"site-header\"><div class=\"wrap header-bar\"><a class=\"brand-lockup\" href=\"/\"><img class=\"site-logo\" src=\"/logo.svg\" alt=\"hu13 logo\" width=\"48\" height=\"48\"></a><span class=\"sep\" aria-hidden=\"true\">•</span><nav class=\"site-nav nav-scroll\" aria-label=\"Site\"><div class=\"nav-scroll-row\">{nav_active}</div></nav></div></header>\n\297297 <main class=\"wrap\">\n{body}\n</main>\n\298298 {footer}\n\299299 </body>\n\@@ -583,6 +583,52 @@ mod tests {583583 assert!(!provenance_footer(Some("main"), Some("")).contains('@'));584584 }585585 586+ #[test]587+ fn finish_gap_vars_logo_footer() {588+ // m6 finish (f0rge #50 + #51 + #52): unified head gaps via vars,589+ // 48px logo, right footer — all in one CSS + header assert.590+ let css = include_str!("../static/style.css");591+ // #50: single vars, mobile tighter (.75/1rem) via :root, desktop592+ // looser (1.25/1.5rem) via the existing min-width breakpoint.593+ assert!(css.contains("--gap-head"), "gap-head var");594+ assert!(css.contains("--gap-content"), "gap-content var");595+ assert!(css.contains("--gap-head:.75rem"), "mobile head gap");596+ assert!(css.contains("--gap-content:1rem"), "mobile content gap");597+ assert!(css.contains("--gap-head:1.25rem"), "desktop head gap");598+ assert!(css.contains("--gap-content:1.5rem"), "desktop content gap");599+ assert!(600+ css.contains(".page-head") && css.contains("margin-top:var(--gap-head)"),601+ "page-head uses gap-head"602+ );603+ assert!(604+ css.contains(".page-head") && css.contains("margin-bottom:var(--gap-content)"),605+ "page-head uses gap-content"606+ );607+ assert!(608+ css.contains(".repo-head") && css.contains("margin-top:var(--gap-head)"),609+ "repo-head uses gap-head"610+ );611+ assert!(612+ css.contains("main>h1:first-child") && css.contains("margin-top:var(--gap-head)"),613+ "bare h1 shares nav->head gap"614+ );615+ // #51: 48px logo (was 24px / 1.5rem), vector kept, header centers.616+ assert!(css.contains(".site-logo"), "logo rule");617+ assert!(css.contains("width:3rem"), "logo 48px width");618+ assert!(css.contains("height:3rem"), "logo 48px height");619+ assert!(!css.contains("width:1.5rem"), "old 24px gone");620+ let html = page("t", "<nav></nav>", "<p>b</p>");621+ assert!(622+ html.contains("width=\"48\" height=\"48\""),623+ "header img 48px attrs"624+ );625+ // #52: footer provenance sits right, both themes (pure CSS var block).626+ assert!(627+ css.contains(".footer-provenance") && css.contains("text-align:right"),628+ "footer right-aligned"629+ );630+ }631+586632 #[test]587633 fn provenance_footer_names_snapshot_and_escapes() {588634 // m6 footer-line (f0rge #41): build pages name branch @ sha;
src/releases.rs+1 −1
--- a/src/releases.rs+++ b/src/releases.rs@@ -391,7 +391,7 @@ pub fn render_loaded(391391 description: Option<&str>,392392 ) -> Result<(), String> {393393 let root = repo.join(".forge").join("releases");394- let mut body = String::from("<h1>Releases</h1>\n");394+ let mut body = String::from("<div class=\"page-head\"><h1>Releases</h1></div>\n");395395 if releases.is_empty() {396396 body.push_str("<p class=\"muted\">No releases yet.</p>\n");397397 } else {
src/wiki.rs+1 −1
--- a/src/wiki.rs+++ b/src/wiki.rs@@ -275,7 +275,7 @@ pub fn render(275275 let wiki_dir = out.join("wiki");276276 std::fs::create_dir_all(&wiki_dir).map_err(|e| format!("hu13: create wiki dir: {e}"))?;277277 278- let mut body = String::from("<h1>Wiki</h1>\n");278+ let mut body = String::from("<div class=\"page-head\"><h1>Wiki</h1></div>\n");279279 if pages.is_empty() {280280 body.push_str("<p class=\"muted\">No wiki pages yet.</p>\n");281281 } else {
static/style.css+9 −6
--- a/static/style.css+++ b/static/style.css@@ -16,6 +16,8 @@1616 --border:#292e42;1717 --visited:#9aa5ce;1818 --btn-fg:#16161e;19+ --gap-head:.75rem;20+ --gap-content:1rem;1921 --diff-add:#9ece6a;2022 --diff-add-bg:#233328;2123 --diff-del:#f7768e;@@ -68,7 +70,7 @@ main{padding:1.25rem 0 3rem;min-width:0}6870 CSS margin-top (never <br> spacers); the base stays tight for 360px6971 and wider screens get a touch more. Title is explicit 2rem so it7072 reads as the page title on every viewport. */71-.repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0;margin-top:.75rem}73+.repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0;margin-top:var(--gap-head);margin-bottom:var(--gap-content)}7274 .repo-head h1{margin:0 0 .25rem;overflow-wrap:anywhere;font-size:2rem;line-height:1.2}7375 .clone,.repo-meta{margin:.25rem 0;min-width:0}7476 .clone code,.repo-meta code{overflow-wrap:anywhere;word-break:break-all}@@ -123,7 +125,7 @@ footer.site-footer{border-top:1px solid var(--border)}123125 footer.site-footer .wrap{padding:1rem}124126 /* Provenance line (m6/footer-line, f0rge #41): the single muted line125127 inside the existing footer block — small, no extra whitespace. */126-footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}128+footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem;text-align:right}127129 /* Navbar one-bar (m6/navbar, f0rge #18): logo hu13 • scrollable nav • repo.128130 Own classes only (.header-bar/.brand-lockup/.site-logo/.hu13-word/.sep/129131 .nav-scroll/.nav-scroll-row); earlier rules above are untouched. The nav@@ -132,7 +134,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}132134 .site-header .wrap.header-bar{display:flex;flex-wrap:nowrap;align-items:center;gap:.5rem .75rem}133135 .brand-lockup{display:inline-flex;align-items:center;gap:.5rem;flex:none;color:var(--fg);text-decoration:none;min-height:40px}134136 .brand-lockup:visited{color:var(--fg)}135-.site-logo{display:block;width:1.5rem;height:1.5rem;flex:none}137+.site-logo{display:block;width:3rem;height:3rem;flex:none}136138 .hu13-word{font-weight:700;color:var(--blue)}137139 .sep{flex:none;color:var(--muted)}138140 .site-header .site-nav.nav-scroll{flex:1 1 auto;min-width:0;background:none;border-bottom:none;overflow-x:auto;scroll-snap-type:none;-webkit-overflow-scrolling:touch}@@ -144,7 +146,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}144146 @media (min-width:768px){145147 .cols{grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)}146148 .site-nav .wrap{gap:.25rem 1.25rem}147- .repo-head{margin-top:1.25rem}149+ :root{--gap-head:1.25rem;--gap-content:1.5rem}148150 main{padding:1.5rem 0 3rem}149151 }150152 /* Code tab two columns (m6/code-cols, f0rge #45): files + README share@@ -160,7 +162,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}160162 .cols-code{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}161163 }162164 /* Code conventions (m6, f0rge #20): breadcrumbs, file meta, line anchors, clone block. */163-.crumbs{margin:.5rem 0;font-size:.95rem}165+.crumbs{margin:var(--gap-head) 0 .5rem;font-size:.95rem}164166 .crumbs a{color:var(--cyan);text-decoration:none}165167 .crumbs a:hover,.crumbs a:focus{text-decoration:underline}166168 .file-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem 1rem;margin:.5rem 0;padding:.5rem .75rem;background:var(--bg-alt);border:1px solid var(--border)}@@ -195,8 +197,9 @@ a.ln:hover,a.ln:focus{text-decoration:underline}195197 .table-scroll td.trunc-title,.trunc-title{max-width:60ch}196198 /* Page titles with inline buttons (m6/table-scroll, f0rge #31): h1 +197199 button share one flex row; wrap allowed on 360px but stays adjacent. */198-.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem}200+.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem;margin-top:var(--gap-head);margin-bottom:var(--gap-content)}199201 .page-head h1{margin:0}202+main>h1:first-child{margin-top:var(--gap-head);margin-bottom:var(--gap-content)}200203 .page-head .btn-new{flex:none}201204 202205 /* Code-gitea (m6, f0rge #33 + #34): file TABLE, language bar, clone details. */
tests/code_space.rs+36 −21
--- a/tests/code_space.rs+++ b/tests/code_space.rs@@ -93,22 +93,26 @@ fn parse_rem(value: &str) -> Option<f32> {9393 #[test]9494 fn repo_head_spacing_is_css_margin_not_br_tags() {9595 // Structure assert: the code tab keeps its stacked repo-head96- // (name + meta + clone block) with air from CSS margin — no `<br>`97- // spacers between the navbar and the repo-head.96+ // (name + clone summary with branch/CI) with air from CSS margin —97+ // no `<br>` spacers between the navbar and the repo-head.98+ // (m6 finish, f0rge #53: branch/sha/CI live ONLY in the clone summary.)9899 let out = build_fixture();99100 let index = std::fs::read_to_string(out.join("index.html")).expect("index");100101 let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");101- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;102- let head = &index[head_start..head_end];102+ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;103+ let head = &index[head_start..details_end];103104 assert!(head.contains("<h1>"), "repo-head keeps its title");104105 assert!(105106 !head.contains("<br"),106107 "spacing is CSS margin, not <br> tags"107108 );108- assert!(109- head.contains("repo-meta"),110- "branch/sha meta line kept inside repo-head"111- );109+ assert!(!index.contains("repo-meta"), "no dupe repo-meta line");110+ assert!(!index.contains("<p>CI:"), "no dupe CI line");111+ let s0 = index.find("<summary>Clone").expect("summary");112+ let s1 = index[s0..].find("</summary>").expect("summary end") + s0;113+ let summary = &index[s0..s1];114+ assert!(summary.contains("main"), "branch in summary");115+ assert!(summary.contains("CI"), "CI in summary");112116 // The header bar ends (</header>) before <main> carries the repo-head:113117 // the break between nav and repo-head is a block boundary + margin.114118 let header_end = index.find("</header>").expect("header close");@@ -122,13 +126,18 @@ fn repo_head_spacing_is_css_margin_not_br_tags() {122126 "no spacer tags between navbar and repo-head"123127 );124128 125- // CSS assert: `.repo-head` carries the top margin.129+ // CSS assert: `.repo-head` carries the top margin via --gap-head.126130 let css = css();127131 let base = css.split("@media (min-width").next().expect("base css");128132 let margin = rule_value(base, ".repo-head", "margin-top").expect("repo-head margin-top");129133 assert!(130- parse_rem(&margin).is_some_and(|v| v > 0.0),131- "repo-head margin-top is positive breathing room, got {margin:?}"134+ margin.contains("var(--gap-head)"),135+ "repo-head margin-top uses --gap-head, got {margin:?}"136+ );137+ let gap = rule_value(base, ":root{", "--gap-head").expect("gap-head var");138+ assert!(139+ parse_rem(&gap).is_some_and(|v| v > 0.0),140+ "gap-head is positive breathing room, got {gap:?}"132141 );133142 }134143 @@ -154,15 +163,17 @@ fn repo_head_title_reads_as_page_title() {154163 #[test]155164 fn mobile_stays_tight_wider_screens_get_more() {156165 // 360px must not drown in whitespace: the base (pre-media-query)157- // margin stays modest; >=768px may open up a touch more.166+ // --gap-head stays modest; >=768px may open up a touch more.167+ // (m6 finish, f0rge #50: gaps ride on --gap-head/--gap-content vars.)158168 let css = css();169+ assert!(css.contains("--gap-head"), "gap-head var");170+ assert!(css.contains("--gap-content"), "gap-content var");159171 let base = css.split("@media (min-width").next().expect("base css");160- let base_margin =161- rule_value(base, ".repo-head", "margin-top").expect("base repo-head margin-top");162- let base_rem = parse_rem(&base_margin).expect("base margin in rem");172+ let base_gap = rule_value(base, ":root{", "--gap-head").expect("base gap-head");173+ let base_rem = parse_rem(&base_gap).expect("base gap in rem");163174 assert!(164175 base_rem <= 1.0,165- "mobile margin-top stays tight (<=1rem), got {base_margin:?}"176+ "mobile gap-head stays tight (<=1rem), got {base_gap:?}"166177 );167178 let desktop = css168179 .split("@media (min-width:768px)")@@ -170,15 +181,19 @@ fn mobile_stays_tight_wider_screens_get_more() {170181 .expect("768px breakpoint");171182 // Only the current breakpoint block: cut before the next @media.172183 let block = desktop.split("@media").next().expect("768 block");173- let wide_margin =174- rule_value(block, ".repo-head", "margin-top").expect("wide repo-head margin-top");175- let wide_rem = parse_rem(&wide_margin).expect("wide margin in rem");184+ let wide_gap = rule_value(block, ":root{", "--gap-head").expect("wide gap-head");185+ let wide_rem = parse_rem(&wide_gap).expect("wide gap in rem");176186 assert!(177187 wide_rem >= base_rem,178- "wider screens keep at least base air ({wide_margin:?} vs {base_margin:?})"188+ "wider screens keep at least base air ({wide_gap:?} vs {base_gap:?})"179189 );180190 assert!(181191 wide_rem <= 2.0,182- "even wide stays modest (<=2rem), got {wide_margin:?}"192+ "even wide stays modest (<=2rem), got {wide_gap:?}"193+ );194+ // Heads share the var (single pattern, not per-page px).195+ assert!(196+ base.contains("margin-top:var(--gap-head)"),197+ "heads use gap-head var"183198 );184199 }
tests/forge_readers.rs+12 −3
--- a/tests/forge_readers.rs+++ b/tests/forge_readers.rs@@ -316,10 +316,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {316316 let index = read("index.html");317317 assert!(index.contains("issues (1)"), "nav open issue count");318318 assert!(index.contains("pulls (1)"), "nav open pull count");319- assert!(index.contains("repo-meta"), "branch/sha meta kept");319+ assert!(320+ !index.contains("repo-meta"),321+ "no dupe repo-meta (f0rge #53)"322+ );323+ assert!(!index.contains("<p>CI:"), "no dupe CI line (f0rge #53)");324+ let summ = index.find("<summary>Clone").expect("summary");325+ let send = index[summ..].find("</summary>").expect("summary end") + summ;326+ let summary = &index[summ..send];327+ assert!(summary.contains("CI"), "CI in summary");328+ assert!(summary.contains("badge fail"), "fail badge in summary");320329 let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");321- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;322- let head = &index[head_start..head_end];330+ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;331+ let head = &index[head_start..details_end];323332 for needle in ["issues (", "pulls (", "tasks (", "releases ("] {324333 assert!(!head.contains(needle), "code tab drops {needle}");325334 }
tests/responsive.rs+5 −1
--- a/tests/responsive.rs+++ b/tests/responsive.rs@@ -233,9 +233,13 @@ fn touch_targets_and_stacking_header() {233233 }234234 let out = build_fixture();235235 let index = std::fs::read_to_string(out.join("index.html")).expect("index");236- for needle in ["repo-head", "cols", "repo-meta"] {236+ for needle in ["repo-head", "cols", "clonebox"] {237237 assert!(index.contains(needle), "index.html has {needle}");238238 }239+ assert!(240+ !index.contains("repo-meta"),241+ "no dupe repo-meta (f0rge #53)"242+ );239243 }240244 241245 #[test]