From 9153d223284229667d59d9afb9d4696fc6c839f7 Mon Sep 17 00:00:00 2001 From: gl0w Date: Thu, 10 Sep 2026 09:42:20 +0100 Subject: [PATCH] m6/code-cols: files+README side by side on desktop --- src/build.rs | 7 +- static/style.css | 9 +++ tests/code_cols.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 tests/code_cols.rs diff --git a/src/build.rs b/src/build.rs index 20ca6f5..6a12e44 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1100,7 +1100,8 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result\n"); - // Single column on mobile; tree+readme sit side by side only at >=768px. + // Stacked below 1100px; files + README sit side by side only at + // >=1100px via .cols-code (m6/code-cols, f0rge #45, 7/12 + 5/12). // Code tab file TABLE (m6 code-gitea, f0rge #33, Gitea-style) plus the // language mix bar (bytes share, Tokyo Night hues, no JS). // Language math (m6, f0rge #37): tracked files only (via gitx, so @@ -1113,10 +1114,10 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result = lang_rows(&blobs, &ignored); let lang_section = language_bar(&lang_files); - body.push_str("

Files

\n"); + body.push_str("

Files

\n"); body.push_str(&file_table_html("", &root_entries, None, &history)); body.push_str(&lang_section); - body.push_str("
\n

README

\n"); + body.push_str("
\n

README

\n"); match &readme { Some((path, blob)) => { body.push_str(&format!( diff --git a/static/style.css b/static/style.css index 17175e3..d645df0 100644 --- a/static/style.css +++ b/static/style.css @@ -147,8 +147,17 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem} .repo-head{margin-top:1.25rem} main{padding:1.5rem 0 3rem} } +/* Code tab two columns (m6/code-cols, f0rge #45): files + README share + the existing .cols grid; the code variant stays stacked below 1100px + so 360px-1099px never squeezes the file table. At >=1100px files take + 7/12 and README 5/12 for a readable line length. Stacked override first + so the later 1100px rule wins the cascade on desktop. */ +@media (min-width:768px){ + .cols-code{grid-template-columns:minmax(0,1fr)} +} @media (min-width:1100px){ .wrap{max-width:72rem;padding:0 1.5rem} + .cols-code{grid-template-columns:minmax(0,7fr) minmax(0,5fr)} } /* Code conventions (m6, f0rge #20): breadcrumbs, file meta, line anchors, clone block. */ .crumbs{margin:.5rem 0;font-size:.95rem} diff --git a/tests/code_cols.rs b/tests/code_cols.rs new file mode 100644 index 0000000..a21ebf3 --- /dev/null +++ b/tests/code_cols.rs @@ -0,0 +1,160 @@ +//! Code tab two columns (m6, f0rge issue #45): +//! files + README share the existing `.cols` grid, side by side only at +//! >=1100px (files 7/12, README 5/12), stacked below. +//! +//! Builds a small fixture repo with the real binary and string-asserts on +//! the emitted HTML + `static/style.css` (no rendering engine needed). + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn bin() -> &'static str { + env!("CARGO_BIN_EXE_hu13") +} + +fn manifest() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +fn fresh(prefix: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "hu13-codecols-{prefix}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + )); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +fn git(repo: &Path, args: &[&str]) { + let st = Command::new("git") + .arg("-C") + .arg(repo) + .args(args) + .status() + .expect("spawn git"); + assert!(st.success(), "git {args:?} failed"); +} + +fn fixture_repo() -> PathBuf { + let dir = fresh("repo"); + git(&dir, &["init", "-b", "main"]); + git(&dir, &["config", "user.name", "hu13-test"]); + git(&dir, &["config", "user.email", "hu13@test.local"]); + git(&dir, &["config", "commit.gpgsign", "false"]); + std::fs::write(dir.join("README.md"), "# cols fixture\n\nhello readme\n").unwrap(); + std::fs::write(dir.join("hello.txt"), "hello\n").unwrap(); + git(&dir, &["add", "-A"]); + git(&dir, &["commit", "-m", "init"]); + dir +} + +fn build_fixture() -> PathBuf { + let repo = fixture_repo(); + let out = fresh("out"); + let st = Command::new(bin()) + .arg("build") + .arg(&repo) + .arg("-o") + .arg(&out) + .status() + .expect("spawn hu13 build"); + assert!(st.success(), "fixture build exits 0"); + out +} + +fn css() -> String { + std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css") +} + +#[test] +fn code_tab_wraps_files_and_readme_in_cols_code() { + let out = build_fixture(); + let index = std::fs::read_to_string(out.join("index.html")).expect("index"); + // Wrapper reuses the existing .cols pattern with the code variant. + assert!( + index.contains("
"), + "code tab wraps files + README in .cols.cols-code" + ); + // Two sections in order: files (table) left, README right. + let files_pos = index + .find("
") + .expect("files section"); + let readme_pos = index + .find("
") + .expect("readme section"); + assert!( + files_pos < readme_pos, + "files section comes before README section" + ); + let files_block = &index[files_pos..readme_pos]; + assert!( + files_block.contains("

Files

"), + "files section keeps its heading" + ); + assert!( + files_block.contains("file-table"), + "file table lives in the files (left) column" + ); + let readme_block = &index[readme_pos..]; + assert!( + readme_block.contains("

README

"), + "README section keeps its heading" + ); + assert!( + readme_block.contains("cols fixture"), + "README body renders in the right column" + ); +} + +#[test] +fn code_cols_breakpoint_is_1100px_stacked_below() { + let css = css(); + // Mobile-first base: .cols is a single stacked column. + let base = css.split("@media (min-width").next().expect("base css"); + assert!( + base.contains(".cols{display:grid;grid-template-columns:minmax(0,1fr)"), + "base .cols stays stacked single-column" + ); + // The code variant stays stacked through the 768px breakpoint + // (overrides the generic .cols side-by-side there). + assert!( + css.contains("@media (min-width:768px)"), + "768px breakpoint kept" + ); + assert!( + css.contains("@media (min-width:1100px)"), + "1100px breakpoint present" + ); + let stacked_rule = ".cols-code{grid-template-columns:minmax(0,1fr)}"; + assert!( + css.contains(stacked_rule), + "cols-code stays stacked below 1100px, got no {stacked_rule:?}" + ); + // Desktop: files 7/12 + README 5/12 inside the 1100px block. + let wide_rule = ".cols-code{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}"; + assert!( + css.contains(wide_rule), + "cols-code is 7/12 + 5/12 at >=1100px, got no {wide_rule:?}" + ); + let block_1100 = css + .split("@media (min-width:1100px)") + .nth(1) + .expect("1100px block"); + let first_chunk = block_1100.split("@media").next().expect("1100 chunk"); + assert!( + first_chunk.contains(".cols-code"), + "7/5 rule lives in the 1100px block" + ); + // Cascade order: the stacked 768px override comes before the 1100px + // two-column rule, so desktop wins and 768px–1099px stays stacked. + let stacked_pos = css.find(stacked_rule).expect("stacked rule pos"); + let wide_pos = css.find(wide_rule).expect("wide rule pos"); + assert!( + stacked_pos < wide_pos, + "stacked override precedes the 1100px two-column rule" + ); +} -- 2.55.0