#57 Responsive: nav wraps, file table drops commit column, clone summary segments (#75)

mergedpassopened
details
branches
master ← issue-75-responsive
issues
1489b1c1-1014-4bb1-b28d-3eca6f811267
updated
id
5cfbd5b6-7bdf-45bb-8420-629541cc40f2

Patch

src/build.rs+11 −8
diff --git a/src/build.rs b/src/build.rsindex a813a50..352fc43 100644--- a/src/build.rs+++ b/src/build.rs@@ -925,7 +925,7 @@ pub fn clone_block(925925     let badge = crate::forge::verdict_badge(ci_verdict);926926     let mut out = format!(927927         "<details class=\"clonebox clone-block\" aria-label=\"Clone and download\">\n\928-         <summary>Clone &amp; download \u{b7} {} @ {} \u{b7} CI {}</summary>\n<div class=\"clone-body\">\n",928+         <summary><span class=\"cb-seg\">Clone &amp; download</span><span class=\"cb-seg\">{} @ {}</span><span class=\"cb-seg\">CI {}</span></summary>\n<div class=\"clone-body\">\n",929929         esc(branch),930930         esc(short_sha),931931         badge@@ -1651,7 +1651,9 @@ mod tests {16511651         assert!(!index.contains("repo-meta"), "no dupe repo-meta line");16521652         assert!(!index.contains("<p>CI:"), "no dupe CI line");16531653         assert!(index.contains("badge"), "CI verdict badge in summary");1654-        let summ = index.find("<summary>Clone").expect("summary");1654+        let summ = index1655+            .find("<summary><span class=\"cb-seg\">Clone")1656+            .expect("summary");16551657         let send = index[summ..].find("</summary>").expect("summary end") + summ;16561658         let summary = &index[summ..send];16571659         assert!(summary.contains("main"), "branch in summary");@@ -2393,10 +2395,7 @@ mod tests {23932395         assert!(index.contains("<details"), "details present");23942396         assert!(index.contains("class=\"clonebox"), "clonebox class");23952397         assert!(index.contains("clone-block"), "clone-block class kept");2396-        assert!(2397-            index.contains("<summary>Clone &amp; download"),2398-            "summary label"2399-        );2398+        assert!(index.contains("Clone &amp; download"), "summary label");24002399         assert!(index.contains("main"), "branch hint in summary");24012400         // Closed by default: no `open` attr on the clone details.24022401         let d0 = index@@ -2475,7 +2474,9 @@ mod tests {24752474             let out = mktemp(&format!("out-ci-{verdict}"));24762475             run_build(&repo, &out, &opts()).expect("build");24772476             let index = std::fs::read_to_string(out.join("index.html")).expect("index");2478-            let summ = index.find("<summary>Clone").expect("summary");2477+            let summ = index2478+                .find("<summary><span class=\"cb-seg\">Clone")2479+                .expect("summary");24792480             let send = index[summ..].find("</summary>").expect("summary end") + summ;24802481             let summary = &index[summ..send];24812482             assert!(@@ -2696,7 +2697,9 @@ mod tests {26962697         // Code tab: repo-head with h1, summary carries branch @ sha + CI.26972698         let head_pos = index.find("<div class=\"repo-head\">").expect("repo-head");26982699         assert!(index[head_pos..].contains("<h1>"), "code h1 in repo-head");2699-        let s0 = index.find("<summary>Clone").expect("summary");2700+        let s0 = index2701+            .find("<summary><span class=\"cb-seg\">Clone")2702+            .expect("summary");27002703         let s1 = index[s0..].find("</summary>").expect("summary end") + s0;27012704         let summary = &index[s0..s1];27022705         assert!(summary.contains("Clone &amp; download"), "summary label");
static/style.css+19 −1
diff --git a/static/style.css b/static/style.cssindex ff5d619..95af580 100644--- a/static/style.css+++ b/static/style.css@@ -279,7 +279,9 @@ main>h1:first-child{margin-top:var(--gap-head);margin-bottom:var(--gap-content)}279279 .lang-legend li{display:inline-flex;align-items:center;gap:.35rem}280280 .lang-dot{display:inline-block;width:.75rem;height:.75rem;flex:none}281281 details.clonebox{margin:1rem 0;padding:.25rem 1rem 1rem;background:var(--bg-alt);border:1px solid var(--border)}282-details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan);margin:0}282+details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan);margin:0;display:flex;flex-wrap:wrap;align-items:center;gap:.15rem .5rem}283+.cb-seg{white-space:nowrap}284+.cb-seg+.cb-seg::before{content:"·";margin-right:.5rem;color:var(--fg-dim)}283285 details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px}284286 /* Clone gap (m6/code-bits, f0rge #44): no blank line under summary when285287    collapsed — collapsed padding stays tight; open gap lives on the@@ -367,3 +369,19 @@ pre.diff{margin:0;border:0;background:var(--surface)}367369 .diff-del-row{background:var(--diff-del-bg)}368370 .diff-file{color:var(--diff-file);font-weight:600}369371 .df-meta{color:var(--fg-dim)}372+373+/* Responsive fixes (f0rge #75): every nav destination visible at 390px,374+   file table keeps Name + Age, meta chips never overflow. */375+@media (max-width:767px){376+  .site-header .wrap.header-bar{flex-wrap:wrap}377+  .site-header .sep{display:none}378+  .site-header .site-nav.nav-scroll{flex:1 1 100%;overflow-x:visible;border-top:1px solid var(--border);margin-top:var(--s1);padding-top:var(--s1)}379+  .nav-scroll-row{flex-wrap:wrap;width:auto;min-width:0;white-space:normal;column-gap:.9rem;row-gap:.1rem}380+  /* Dense file table: drop "Last commit" below 768px instead of wrapping381+     commit subjects into four lines or clipping the age column. */382+  .file-table th:nth-child(2),.file-table td:nth-child(2){display:none}383+  .file-table td.title{white-space:nowrap}384+  .file-scroll{overflow-x:auto}385+  .meta .chip,.meta .pill{max-width:100%;overflow-wrap:anywhere}386+  .clonebox>summary{font-weight:700}387+}
tests/code_space.rs+3 −1
diff --git a/tests/code_space.rs b/tests/code_space.rsindex 02e1aab..d8e406c 100644--- a/tests/code_space.rs+++ b/tests/code_space.rs@@ -113,7 +113,9 @@ fn repo_head_spacing_is_css_margin_not_br_tags() {113113     );114114     assert!(!index.contains("repo-meta"), "no dupe repo-meta line");115115     assert!(!index.contains("<p>CI:"), "no dupe CI line");116-    let s0 = index.find("<summary>Clone").expect("summary");116+    let s0 = index117+        .find("<summary><span class=\"cb-seg\">Clone")118+        .expect("summary");117119     let s1 = index[s0..].find("</summary>").expect("summary end") + s0;118120     let summary = &index[s0..s1];119121     assert!(summary.contains("main"), "branch in summary");
tests/forge_readers.rs+3 −1
diff --git a/tests/forge_readers.rs b/tests/forge_readers.rsindex f0b7baa..276d52d 100644--- a/tests/forge_readers.rs+++ b/tests/forge_readers.rs@@ -326,7 +326,9 @@ fn forge_fixture_renders_counts_numbers_and_ci() {326326         "no dupe repo-meta (f0rge #53)"327327     );328328     assert!(!index.contains("<p>CI:"), "no dupe CI line (f0rge #53)");329-    let summ = index.find("<summary>Clone").expect("summary");329+    let summ = index330+        .find("<summary><span class=\"cb-seg\">Clone")331+        .expect("summary");330332     let send = index[summ..].find("</summary>").expect("summary end") + summ;331333     let summary = &index[summ..send];332334     assert!(summary.contains("CI"), "CI in summary");
tests/light_theme.rs+13 −13
diff --git a/tests/light_theme.rs b/tests/light_theme.rsindex d7cb27c..42f309b 100644--- a/tests/light_theme.rs+++ b/tests/light_theme.rs@@ -17,18 +17,18 @@ fn css() -> String {1717 /// Strip `/* ... */` comments so palette hex in prose is not mistaken1818 /// for hardcoded rule colors.1919 fn without_comments(s: &str) -> String {20+    let chars: Vec<char> = s.chars().collect();2021     let mut out = String::with_capacity(s.len());21-    let bytes = s.as_bytes();2222     let mut i = 0;23-    while i < bytes.len() {24-        if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {23+    while i < chars.len() {24+        if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {2525             i += 2;26-            while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {26+            while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {2727                 i += 1;2828             }2929             i += 2;3030         } else {31-            out.push(bytes[i] as char);31+            out.push(chars[i]);3232             i += 1;3333         }3434     }@@ -38,18 +38,18 @@ fn without_comments(s: &str) -> String {3838 /// Remove every `:root { ... }` block (brace-matched) so var definitions3939 /// may carry palette hex; what remains must reference vars, not hex.4040 fn without_root_blocks(s: &str) -> String {41+    let chars: Vec<char> = s.chars().collect();4142     let mut out = String::with_capacity(s.len());42-    let bytes = s.as_bytes();4343     let mut i = 0;44-    while i < bytes.len() {45-        if s[i..].starts_with(":root") {46-            if let Some(open) = s[i..].find('{') {44+    while i < chars.len() {45+        if chars[i..].iter().take(5).collect::<String>() == ":root" {46+            if let Some(open) = chars[i..].iter().position(|c| *c == '{') {4747                 let mut depth = 0;4848                 let mut j = i + open;49-                while j < bytes.len() {50-                    if bytes[j] == b'{' {49+                while j < chars.len() {50+                    if chars[j] == '{' {5151                         depth += 1;52-                    } else if bytes[j] == b'}' {52+                    } else if chars[j] == '}' {5353                         depth -= 1;5454                         if depth == 0 {5555                             break;@@ -61,7 +61,7 @@ fn without_root_blocks(s: &str) -> String {6161                 continue;6262             }6363         }64-        out.push(bytes[i] as char);64+        out.push(chars[i]);6565         i += 1;6666     }6767     out
tests/responsive.rs+47 −1
diff --git a/tests/responsive.rs b/tests/responsive.rsindex 9c333b7..3a62e8c 100644--- a/tests/responsive.rs+++ b/tests/responsive.rs@@ -464,3 +464,50 @@ fn zero_bare_tables_in_sources() {464464     }465465     assert!(checked >= 5, "audit saw all list tables, got {checked}");466466 }467+468+#[test]469+fn mobile_nav_wraps_and_file_table_keeps_name_and_age() {470+    // f0rge #75: no destination may be clipped at 390px; the dense file471+    // table drops "Last commit" instead of wrapping/clipping.472+    let css = css();473+    let at = css.find("@media (max-width:767px)").expect("narrow block");474+    let narrow = &css[at..];475+    assert!(476+        narrow.contains(".nav-scroll-row{flex-wrap:wrap"),477+        "nav wraps into multiple rows on narrow: {narrow}"478+    );479+    assert!(480+        narrow.contains(".site-nav.nav-scroll{flex:1 1 100%;overflow-x:visible"),481+        "nav gets its own row and is not clipped"482+    );483+    assert!(484+        narrow.contains(".file-table th:nth-child(2),.file-table td:nth-child(2){display:none}"),485+        "file table drops Last commit on mobile"486+    );487+    assert!(488+        css.contains(".cb-seg{white-space:nowrap}"),489+        "clone summary segments never wrap mid-token"490+    );491+    let out = build_fixture();492+    let index = std::fs::read_to_string(out.join("index.html")).expect("index");493+    assert_eq!(494+        index.matches("class=\"cb-seg\"").count(),495+        3,496+        "clone summary has three no-wrap segments"497+    );498+    assert!(499+        index.contains("class=\"cb-seg\">CI <span class=\"badge"),500+        "CI badge stays inside its segment"501+    );502+    // All nav destinations are present in the DOM (wrapping is CSS-only).503+    for tab in [504+        "/",505+        "/commits.html",506+        "/issues/index.html",507+        "/pulls/index.html",508+        "/ci/index.html",509+        "/agents.html",510+    ] {511+        assert!(index.contains(&format!("href=\"{tab}\"")), "nav has {tab}");512+    }513+}467-- 2.56.0

Comments (0)

No comments yet.

Reviews (0)

No reviews yet.

all pulls