From 3c95203548237b94d7a53bb6870f1b79299113d4 Mon Sep 17 00:00:00 2001 From: hu13-test Date: Tue, 27 Oct 2026 16:32:09 +0000 Subject: [PATCH] responsive: nav wraps, file table drops commit column, clone summary segments (#75) - <=767px: header wraps into brand row + nav row; nav links wrap inside the row (overflow visible) instead of being clipped in a scroll strip, so all 8 destinations are reachable at 320/390px. The stray sep dot is hidden when the nav gets its own row. - File table hides 'Last commit' on narrow (Name + Age stay, both links) instead of wrapping subjects into four lines and clipping Age. - Clone summary is three .cb-seg spans (Clone & download / branch @ sha / CI badge) with white-space:nowrap + a CSS bullet between them, so the badge never splits from 'CI' or wraps mid-token. - Meta chips/pills get max-width:100% + overflow-wrap:anywhere. - Tests: new responsive assertion block; existing summary finders updated for the segmented markup; light_theme CSS helpers are now UTF-8-safe (they sliced bytes as chars). --- src/build.rs | 19 ++++++++++------- static/style.css | 20 +++++++++++++++++- tests/code_space.rs | 4 +++- tests/forge_readers.rs | 4 +++- tests/light_theme.rs | 26 +++++++++++------------ tests/responsive.rs | 47 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/build.rs b/src/build.rs index a813a50..352fc43 100644 --- a/src/build.rs +++ b/src/build.rs @@ -925,7 +925,7 @@ pub fn clone_block( let badge = crate::forge::verdict_badge(ci_verdict); let mut out = format!( "
\n\ - Clone & download \u{b7} {} @ {} \u{b7} CI {}\n
\n", + Clone & download{} @ {}CI {}\n
\n", esc(branch), esc(short_sha), badge @@ -1651,7 +1651,9 @@ mod tests { assert!(!index.contains("repo-meta"), "no dupe repo-meta line"); assert!(!index.contains("

CI:"), "no dupe CI line"); assert!(index.contains("badge"), "CI verdict badge in summary"); - let summ = index.find("

Clone").expect("summary"); + let summ = index + .find("Clone") + .expect("summary"); let send = index[summ..].find("").expect("summary end") + summ; let summary = &index[summ..send]; assert!(summary.contains("main"), "branch in summary"); @@ -2393,10 +2395,7 @@ mod tests { assert!(index.contains("Clone & download"), - "summary label" - ); + assert!(index.contains("Clone & download"), "summary label"); assert!(index.contains("main"), "branch hint in summary"); // Closed by default: no `open` attr on the clone details. let d0 = index @@ -2475,7 +2474,9 @@ mod tests { let out = mktemp(&format!("out-ci-{verdict}")); run_build(&repo, &out, &opts()).expect("build"); let index = std::fs::read_to_string(out.join("index.html")).expect("index"); - let summ = index.find("Clone").expect("summary"); + let summ = index + .find("Clone") + .expect("summary"); let send = index[summ..].find("").expect("summary end") + summ; let summary = &index[summ..send]; assert!( @@ -2696,7 +2697,9 @@ mod tests { // Code tab: repo-head with h1, summary carries branch @ sha + CI. let head_pos = index.find("
").expect("repo-head"); assert!(index[head_pos..].contains("

"), "code h1 in repo-head"); - let s0 = index.find("Clone").expect("summary"); + let s0 = index + .find("Clone") + .expect("summary"); let s1 = index[s0..].find("").expect("summary end") + s0; let summary = &index[s0..s1]; assert!(summary.contains("Clone & download"), "summary label"); diff --git a/static/style.css b/static/style.css index 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)} .lang-legend li{display:inline-flex;align-items:center;gap:.35rem} .lang-dot{display:inline-block;width:.75rem;height:.75rem;flex:none} details.clonebox{margin:1rem 0;padding:.25rem 1rem 1rem;background:var(--bg-alt);border:1px solid var(--border)} -details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan);margin:0} +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} +.cb-seg{white-space:nowrap} +.cb-seg+.cb-seg::before{content:"·";margin-right:.5rem;color:var(--fg-dim)} details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px} /* Clone gap (m6/code-bits, f0rge #44): no blank line under summary when collapsed — collapsed padding stays tight; open gap lives on the @@ -367,3 +369,19 @@ pre.diff{margin:0;border:0;background:var(--surface)} .diff-del-row{background:var(--diff-del-bg)} .diff-file{color:var(--diff-file);font-weight:600} .df-meta{color:var(--fg-dim)} + +/* Responsive fixes (f0rge #75): every nav destination visible at 390px, + file table keeps Name + Age, meta chips never overflow. */ +@media (max-width:767px){ + .site-header .wrap.header-bar{flex-wrap:wrap} + .site-header .sep{display:none} + .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)} + .nav-scroll-row{flex-wrap:wrap;width:auto;min-width:0;white-space:normal;column-gap:.9rem;row-gap:.1rem} + /* Dense file table: drop "Last commit" below 768px instead of wrapping + commit subjects into four lines or clipping the age column. */ + .file-table th:nth-child(2),.file-table td:nth-child(2){display:none} + .file-table td.title{white-space:nowrap} + .file-scroll{overflow-x:auto} + .meta .chip,.meta .pill{max-width:100%;overflow-wrap:anywhere} + .clonebox>summary{font-weight:700} +} diff --git a/tests/code_space.rs b/tests/code_space.rs index 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() { ); assert!(!index.contains("repo-meta"), "no dupe repo-meta line"); assert!(!index.contains("

CI:"), "no dupe CI line"); - let s0 = index.find("

Clone").expect("summary"); + let s0 = index + .find("Clone") + .expect("summary"); let s1 = index[s0..].find("").expect("summary end") + s0; let summary = &index[s0..s1]; assert!(summary.contains("main"), "branch in summary"); diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs index 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() { "no dupe repo-meta (f0rge #53)" ); assert!(!index.contains("

CI:"), "no dupe CI line (f0rge #53)"); - let summ = index.find("

Clone").expect("summary"); + let summ = index + .find("Clone") + .expect("summary"); let send = index[summ..].find("").expect("summary end") + summ; let summary = &index[summ..send]; assert!(summary.contains("CI"), "CI in summary"); diff --git a/tests/light_theme.rs b/tests/light_theme.rs index d7cb27c..42f309b 100644 --- a/tests/light_theme.rs +++ b/tests/light_theme.rs @@ -17,18 +17,18 @@ fn css() -> String { /// Strip `/* ... */` comments so palette hex in prose is not mistaken /// for hardcoded rule colors. fn without_comments(s: &str) -> String { + let chars: Vec = s.chars().collect(); let mut out = String::with_capacity(s.len()); - let bytes = s.as_bytes(); let mut i = 0; - while i < bytes.len() { - if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' { + while i < chars.len() { + if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' { i += 2; - while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') { + while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') { i += 1; } i += 2; } else { - out.push(bytes[i] as char); + out.push(chars[i]); i += 1; } } @@ -38,18 +38,18 @@ fn without_comments(s: &str) -> String { /// Remove every `:root { ... }` block (brace-matched) so var definitions /// may carry palette hex; what remains must reference vars, not hex. fn without_root_blocks(s: &str) -> String { + let chars: Vec = s.chars().collect(); let mut out = String::with_capacity(s.len()); - let bytes = s.as_bytes(); let mut i = 0; - while i < bytes.len() { - if s[i..].starts_with(":root") { - if let Some(open) = s[i..].find('{') { + while i < chars.len() { + if chars[i..].iter().take(5).collect::() == ":root" { + if let Some(open) = chars[i..].iter().position(|c| *c == '{') { let mut depth = 0; let mut j = i + open; - while j < bytes.len() { - if bytes[j] == b'{' { + while j < chars.len() { + if chars[j] == '{' { depth += 1; - } else if bytes[j] == b'}' { + } else if chars[j] == '}' { depth -= 1; if depth == 0 { break; @@ -61,7 +61,7 @@ fn without_root_blocks(s: &str) -> String { continue; } } - out.push(bytes[i] as char); + out.push(chars[i]); i += 1; } out diff --git a/tests/responsive.rs b/tests/responsive.rs index 9c333b7..3a62e8c 100644 --- a/tests/responsive.rs +++ b/tests/responsive.rs @@ -464,3 +464,50 @@ fn zero_bare_tables_in_sources() { } assert!(checked >= 5, "audit saw all list tables, got {checked}"); } + +#[test] +fn mobile_nav_wraps_and_file_table_keeps_name_and_age() { + // f0rge #75: no destination may be clipped at 390px; the dense file + // table drops "Last commit" instead of wrapping/clipping. + let css = css(); + let at = css.find("@media (max-width:767px)").expect("narrow block"); + let narrow = &css[at..]; + assert!( + narrow.contains(".nav-scroll-row{flex-wrap:wrap"), + "nav wraps into multiple rows on narrow: {narrow}" + ); + assert!( + narrow.contains(".site-nav.nav-scroll{flex:1 1 100%;overflow-x:visible"), + "nav gets its own row and is not clipped" + ); + assert!( + narrow.contains(".file-table th:nth-child(2),.file-table td:nth-child(2){display:none}"), + "file table drops Last commit on mobile" + ); + assert!( + css.contains(".cb-seg{white-space:nowrap}"), + "clone summary segments never wrap mid-token" + ); + let out = build_fixture(); + let index = std::fs::read_to_string(out.join("index.html")).expect("index"); + assert_eq!( + index.matches("class=\"cb-seg\"").count(), + 3, + "clone summary has three no-wrap segments" + ); + assert!( + index.contains("class=\"cb-seg\">CI