m6/code-bits: CI badge colors + details gap fix

e8d1601gl0w

all commits

Message

m6/code-bits: CI badge colors + details gap fix

Diff

src/build.rs+60 −3
diff --git a/src/build.rs b/src/build.rsindex 5d176ba..ffd8e86 100644--- a/src/build.rs+++ b/src/build.rs@@ -669,7 +669,7 @@ pub fn clone_block(669669     };670670     let mut out = format!(671671         "<details class=\"clonebox clone-block\" aria-label=\"Clone and download\">\n\672-         <summary>Clone &amp; download <span class=\"muted\">{} @ {}</span></summary>\n",672+         <summary>Clone &amp; download <span class=\"muted\">{} @ {}</span></summary>\n<div class=\"clone-body\">\n",673673         esc(branch),674674         esc(short_sha)675675     );@@ -696,7 +696,7 @@ pub fn clone_block(696696                 esc(url)697697             ));698698         }699-        out.push_str("</details>\n");699+        out.push_str("</div>\n</details>\n");700700         return out;701701     }702702     if let Some(url) = clone_url {@@ -727,7 +727,7 @@ pub fn clone_block(727727              served from this server: ask the host for its address.</p>\n",728728         );729729     }730-    out.push_str("</details>\n");730+    out.push_str("</div>\n</details>\n");731731     out732732 }733733 @@ -1996,6 +1996,63 @@ mod tests {19961996         assert!(bundle_pos < tar_pos, "bundle before tarball");19971997     }19981998 1999+    #[test]2000+    fn code_head_ci_badge_green_red() {2001+        // f0rge #43: code repo-head CI badge is green/red via2002+        // `.badge.pass` / `.badge.fail` (running/cancelled keep tones).2003+        for verdict in ["pass", "fail"] {2004+            let repo = fixture_repo();2005+            let run_dir = repo.join(".forge").join("ci/runs/2026-09-10T00-00-00Z");2006+            std::fs::create_dir_all(&run_dir).unwrap();2007+            std::fs::write(2008+                run_dir.join("summary.md"),2009+                format!(2010+                    "---\nrun: 2026-09-10T00-00-00Z\nverdict: {verdict}\ncreated: 2026-09-10T00:00:00Z\n---\nlog\n"2011+                ),2012+            )2013+            .unwrap();2014+            git(&repo, &["add", "-A"]);2015+            git(&repo, &["commit", "-m", &format!("ci {verdict}")]);2016+            let out = mktemp(&format!("out-ci-{verdict}"));2017+            run_build(&repo, &out, &opts()).expect("build");2018+            let index = std::fs::read_to_string(out.join("index.html")).expect("index");2019+            let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");2020+            let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;2021+            let head = &index[head_start..head_end];2022+            assert!(2023+                head.contains(&format!("badge {verdict}")),2024+                "code head carries badge {verdict}"2025+            );2026+        }2027+    }2028+2029+    #[test]2030+    fn clone_summary_followed_by_body_no_gap() {2031+        // f0rge #44: summary immediately followed by content wrapper —2032+        // no empty <p>, no margin-collapse gap when collapsed.2033+        let (_repo, out) = build_fixture();2034+        let index = std::fs::read_to_string(out.join("index.html")).expect("index");2035+        assert!(2036+            index.contains("</summary>\n<div class=\"clone-body\">"),2037+            "summary immediately followed by clone-body wrapper"2038+        );2039+        let s = index.find("</summary>").expect("summary end");2040+        let after = &index[s..s + 200];2041+        assert!(2042+            !after.contains("<p></p>"),2043+            "no empty paragraph after summary"2044+        );2045+        assert!(2046+            !after.contains("<p> </p>"),2047+            "no blank paragraph after summary"2048+        );2049+        // Wrapper closes before the details closes (structure intact).2050+        assert!(2051+            index.contains("</div>\n</details>"),2052+            "clone-body closes inside details"2053+        );2054+    }2055+19992056     #[test]20002057     fn is_binary_sniffs_first_8kib() {20012058         // f0rge #37: NUL in the first 8KiB means binary for math.
static/style.css+14 −1
diff --git a/static/style.css b/static/style.cssindex fa1b70d..386ad62 100644--- a/static/style.css+++ b/static/style.css@@ -189,9 +189,22 @@ a.ln:hover,a.ln:focus{text-decoration:underline}189189 .lang-legend li{display:inline-flex;align-items:center;gap:.35rem}190190 .lang-dot{display:inline-block;width:.75rem;height:.75rem;flex:none}191191 details.clonebox{margin:1rem 0;padding:.25rem 1rem 1rem;background:var(--bg-alt);border:1px solid var(--border)}192-details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan)}192+details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan);margin:0}193193 details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px}194+/* Clone gap (m6/code-bits, f0rge #44): no blank line under summary when195+   collapsed — collapsed padding stays tight; open gap lives on the196+   wrapper's first child so margins never collapse through summary. */197+details.clonebox:not([open]){padding-bottom:.25rem}198+.clonebox .clone-body{margin:0}199+.clonebox .clone-body>:first-child{margin-top:.75rem}194200 .clone-hint,.clonebox summary .muted{font-weight:400}201+/* CI verdict badges (m6/code-bits, f0rge #43): GitHub-style green/red on202+   the code repo-head; running/cancelled keep muted tones. */203+.badge{display:inline-block;padding:.1em .5em;border:1px solid var(--border);font-weight:700;font-size:.85rem}204+.badge.pass{color:var(--green);border-color:var(--green)}205+.badge.fail{color:var(--red);border-color:var(--red)}206+.badge.running{color:var(--yellow);border-color:var(--yellow)}207+.badge.cancelled{color:var(--muted);border-color:var(--muted)}195208 /* Stacked cards on narrow (m6/no-scroll, f0rge #39): Gitea-style reflow.196209    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.197210    Narrow: no scroll (overflow visible), thead clipped for SR only, each