#28 m6/logo-text

mergedpassopened
details
branches
master ← m6/logo-text
updated
id
a097a750-6241-46a3-8131-1ae2dfb06b6b

Patch

src/build.rs+8 −4
diff --git a/src/build.rs b/src/build.rsindex 5083fa7..82eaf31 100644--- a/src/build.rs+++ b/src/build.rs@@ -370,11 +370,15 @@ fn write_style(out: &Path) -> Result<(), String> {370370     std::fs::write(out.join("style.css"), css).map_err(|e| format!("hu13: write style.css: {e}"))371371 }372372 373-/// Write `out/favicon.svg` + `out/manifest.webmanifest` from the embedded374-/// static files (m6 favicon, f0rge issue #16). SVG-first, zero binary375-/// blobs: the favicon is the hand-written logo mark, and the manifest376-/// points at it (`icons` svg, `short_name` hu13, `display` minimal-ui).373+/// Write `out/logo.svg` + `out/favicon.svg` + `out/manifest.webmanifest`374+/// from the embedded static files (m6 favicon #16; logo-text #27 + #28).375+/// SVG-first, zero binary blobs: the header lockup is `logo.svg`376+/// (diamond-hub + hu13 as <path>, no <text>/fonts), the favicon is the377+/// simplified mark-only variant for 16px, and the manifest points at the378+/// favicon (`icons` svg, `short_name` hu13, `display` minimal-ui).377379 fn write_favicon_and_manifest(out: &Path) -> Result<(), String> {380+    let logo = include_str!("../static/logo.svg");381+    std::fs::write(out.join("logo.svg"), logo).map_err(|e| format!("hu13: write logo.svg: {e}"))?;378382     let icon = include_str!("../static/favicon.svg");379383     std::fs::write(out.join("favicon.svg"), icon)380384         .map_err(|e| format!("hu13: write favicon.svg: {e}"))?;
src/layout.rs+38 −18
diff --git a/src/layout.rs b/src/layout.rsindex af31aa2..430c397 100644--- a/src/layout.rs+++ b/src/layout.rs@@ -87,12 +87,15 @@ pub fn page(title: &str, nav: &str, body: &str) -> String {8787 }8888 8989 /// Render a full HTML page with site identity (m6 header config, f0rge90-/// issues #21 + #24): the header brand shows `site_name`, `<title>` is90+/// issues #21 + #24; minimal header f0rge issues #27 + #28): the header91+/// carries zero names (logo-with-text art only + nav), `<title>` is9192 /// the escaped page `title` (callers build it as `"{page} \u2014 {site}"`9293 /// so the repo name is in every tab), `og:title` mirrors the page title,9394 /// and `og:description` / `meta[name=description]` carry `description`94-/// when present. `site_name` and `description` are escaped (hostile95-/// `<script>` renders as text). `nav`/`body` are inlined as-is like [`page`].95+/// when present. `site_name` still names the title/og surfaces (and the96+/// feed title + code-page repo-head via `build`); it is never rendered in97+/// the header. `description` is escaped (hostile `<script>` renders as98+/// text). `nav`/`body` are inlined as-is like [`page`].9699 ///97100 /// Head standards (m6 favicon, f0rge issue #16) ride along on every page:98101 /// SVG-first favicon (`rel=icon` + `apple-touch-icon` both point at@@ -192,10 +195,12 @@ pub fn page_site(192195     description: Option<&str>,193196 ) -> String {194197     let title_e = esc(title);195-    let brand = esc(site_name);196-    // One-bar header (m6 navbar, f0rge issue #18): the active link (if197-    // any) gets `aria-current="page"` before the nav is inlined. The198-    // favicon doubles as the logo mark (byte-identical to `logo.svg`).198+    // One-bar minimal header (m6 navbar #18, logo-text #27 + #28): the199+    // active link (if any) gets `aria-current="page"` before the nav is200+    // inlined. Logo art is `logo.svg` (diamond-hub + hu13 as <path>, no201+    // <text>/fonts); `favicon.svg` is the simplified mark-only variant202+    // for 16px (see `logo.rs`). Header is logo + nav only: exactly one203+    // `•` separator, no `hu13-word` / `brand` name nodes.199204     let mut head = format!(200205         "<meta charset=\"utf-8\">\n\201206 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\@@ -227,7 +232,7 @@ pub fn page_site(227232 <html lang=\"en\">\n\228233 <head>\n{head}</head>\n\229234 <body>\n\230-<header class=\"site-header\"><div class=\"wrap header-bar\"><a class=\"brand-lockup\" href=\"/\"><img class=\"site-logo\" src=\"/favicon.svg\" alt=\"hu13 logo\" width=\"24\" height=\"24\"><span class=\"hu13-word\">hu13</span></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><span class=\"sep\" aria-hidden=\"true\">•</span><span class=\"brand\">{brand}</span></div></header>\n\235+<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\231236 <main class=\"wrap\">\n{body}\n</main>\n\232237 </body>\n\233238 </html>\n"@@ -301,25 +306,40 @@ mod tests {301306     }302307 303308     #[test]304-    fn header_is_one_bar_with_logo_seps_brand_and_scroll_nav() {305-        // m6 navbar (f0rge issue #18): a single <header> holds logo +306-        // hu13 • scrollable nav • repo brand; no second top-level nav.309+    fn header_is_one_bar_with_logo_sep_and_scroll_nav() {310+        // m6 navbar (#18) + logo-text (#27 + #28): a single <header>311+        // holds logo-with-text • scrollable nav only; no hu13-word or312+        // brand name nodes. The repo name still lives in <title>/og.307313         use super::{active_href, apply_active, page_site};308314         let nav = "<a href=\"/\">code</a> · <a href=\"/commits.html\">commits</a>";309315         let html = page_site("demo", nav, "<p>b</p>", "demo", None);310316         assert_eq!(html.matches("<header").count(), 1, "single header bar");311317         assert!(312-            html.contains("<img class=\"site-logo\" src=\"/favicon.svg\" alt=\"hu13 logo\""),313-            "logo img with alt text"318+            html.contains("<img class=\"site-logo\" src=\"/logo.svg\" alt=\"hu13 logo\""),319+            "logo lockup img with alt text"314320         );315321         assert!(316-            html.contains("<span class=\"hu13-word\">hu13</span>"),317-            "hu13 word"322+            !html.contains("hu13-word"),323+            "no hu13 word node (letters live in logo.svg paths)"318324         );319-        assert!(html.matches("•").count() >= 2, "bullet separators");320325         assert!(321-            html.contains("<span class=\"brand\">demo</span>"),322-            "repo brand kept"326+            !html.contains("<span class=\"brand\">"),327+            "no repo brand node in header"328+        );329+        let h0 = html.find("<header").expect("header open");330+        let h1 = html.find("</header>").expect("header close");331+        let header = &html[h0..h1];332+        assert!(!header.contains(">demo<"), "header carries zero names");333+        assert_eq!(334+            header.matches("•").count(),335+            1,336+            "exactly one sep (logo • nav)"337+        );338+        // Repo name still lives in title + og:title.339+        assert!(html.contains("<title>demo</title>"), "title keeps name");340+        assert!(341+            html.contains("<meta property=\"og:title\" content=\"demo\">"),342+            "og:title keeps name"323343         );324344         assert!(html.contains("nav-scroll"), "nav has scroll class");325345         assert!(html.contains("<nav"), "nav lives inside the header");
src/logo.rs+34 −9
diff --git a/src/logo.rs b/src/logo.rsindex c822220..f580b2b 100644--- a/src/logo.rs+++ b/src/logo.rs@@ -1,12 +1,15 @@1-//! hu13 logo + ASCII banner (m6, f0rge issue #15).1+//! hu13 logo + ASCII banner (m6, f0rge issues #15 + #27 + #28).22 //!3-//! The mark lives hand-written in `static/logo.svg` (square icon, reads at4-//! 16px) plus `static/wordmark.svg` (horizontal lockup for the navbar); the5-//! text banner lives in `static/ascii.txt`.3+//! The lockup lives hand-written in `static/logo.svg` (diamond-hub mark +4+//! hu13 lettered as <path>, no <text>/fonts; reads at 24px header size)5+//! plus `static/wordmark.svg` (legacy horizontal lockup); the simplified6+//! 16px favicon variant lives in `static/favicon.svg` (mark only, no7+//! letters — small sizes stay legible, full lettering stays in logo.svg);8+//! the text banner lives in `static/ascii.txt`.69 //!7-//! [`ASCII_ART`], [`LOGO_SVG`] and [`WORDMARK_SVG`] re-expose those static8-//! files for later issues (navbar #16, favicon #17, wiki README #19, agents9-//! page) with no build step.10+//! [`ASCII_ART`], [`LOGO_SVG`], [`FAVICON_SVG`] and [`WORDMARK_SVG`]11+//! re-expose those static files for later issues (navbar #16, favicon #17,12+//! wiki README #19, agents page) with no build step.1013 //!1114 //! [`ascii_pre`] renders the banner as an escaped `<pre>` block (see1215 //! [`crate::layout::esc`]): the art itself is plain `|/-_` plus spaces so@@ -22,9 +25,14 @@ use crate::layout::esc;2225 pub const ASCII_ART: &str = include_str!("../static/ascii.txt");2326 2427 #[allow(dead_code)]25-/// Square mark, byte-for-byte `static/logo.svg`.28+/// Header lockup, byte-for-byte `static/logo.svg` (mark + hu13 as <path>).2629 pub const LOGO_SVG: &str = include_str!("../static/logo.svg");2730 31+#[allow(dead_code)]32+/// Simplified 16px favicon variant, byte-for-byte `static/favicon.svg`33+/// (mark only, no letters — documented choice for small-size legibility).34+pub const FAVICON_SVG: &str = include_str!("../static/favicon.svg");35+2836 #[allow(dead_code)]2937 /// Horizontal lockup, byte-for-byte `static/wordmark.svg`.3038 pub const WORDMARK_SVG: &str = include_str!("../static/wordmark.svg");@@ -74,9 +82,26 @@ mod tests {7482 7583     #[test]7684     fn svg_consts_match_tiny_marks() {77-        for (name, svg) in [("logo", LOGO_SVG), ("wordmark", WORDMARK_SVG)] {85+        for (name, svg) in [86+            ("logo", LOGO_SVG),87+            ("favicon", FAVICON_SVG),88+            ("wordmark", WORDMARK_SVG),89+        ] {7890             assert!(svg.contains("<svg"), "{name} const holds svg");7991             assert!(svg.len() < 2048, "{name} const stays tiny");8092         }93+        // Logo-text (#27): lockup letters are <path>, never <text>/fonts.94+        assert!(LOGO_SVG.contains("<path"), "logo letters are paths");95+        assert!(!LOGO_SVG.contains("<text"), "logo has no <text>");96+        assert!(97+            !LOGO_SVG.to_lowercase().contains("font"),98+            "logo has no font refs"99+        );100+        // Simplified favicon variant (#27): mark only, no letters.101+        assert!(!FAVICON_SVG.contains("<text"), "favicon has no <text>");102+        assert!(103+            !FAVICON_SVG.contains("<path"),104+            "favicon is mark-only (no letter paths)"105+        );81106     }82107 }
static/favicon.svg+1 −0
diff --git a/static/favicon.svg b/static/favicon.svgindex 35a4426..098e978 100644--- a/static/favicon.svg+++ b/static/favicon.svg@@ -1,4 +1,5 @@11 <?xml version="1.0" encoding="UTF-8"?>2+<!-- hu13 favicon: simplified mark-only variant for 16px legibility. Full lettering lives in logo.svg. -->23 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="hu13 logo">34   <title>hu13</title>45   <rect x="0" y="0" width="32" height="32" fill="#1a1b26"/>
static/logo.svg+9 −4
diff --git a/static/logo.svg b/static/logo.svgindex 35a4426..1e7b01c 100644--- a/static/logo.svg+++ b/static/logo.svg@@ -1,8 +1,13 @@11 <?xml version="1.0" encoding="UTF-8"?>2-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="hu13 logo">2+<!-- hu13 lockup: diamond-hub mark + hu13 lettering as hand-drawn vectors. Reads at 24px header size. -->3+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 32" width="96" height="32" role="img" aria-label="hu13 logo">34   <title>hu13</title>4-  <rect x="0" y="0" width="32" height="32" fill="#1a1b26"/>5-  <rect x="0.5" y="0.5" width="31" height="31" fill="none" stroke="#292e42"/>6-  <polygon points="16,6 26,16 16,26 6,16" fill="none" stroke="#7aa2f7" stroke-width="3"/>5+  <rect x="0" y="0" width="96" height="32" fill="#1a1b26"/>6+  <rect x="0.5" y="0.5" width="95" height="31" fill="none" stroke="#292e42"/>7+  <polygon points="16,4 28,16 16,28 4,16" fill="none" stroke="#7aa2f7" stroke-width="3"/>78   <rect x="13" y="13" width="6" height="6" fill="#7dcfff"/>9+  <path d="M40 8 V24 M40 15 H46 V24" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>10+  <path d="M52 8 V21 H58 V8" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>11+  <path d="M64 10 L66 8 V24 M62 24 H70" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>12+  <path d="M74 8 H80 V14 H74 M74 14 H80 V24 H74" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>813 </svg>
tests/favicon.rs+15 −4
diff --git a/tests/favicon.rs b/tests/favicon.rsindex 4277749..3064d9f 100644--- a/tests/favicon.rs+++ b/tests/favicon.rs@@ -158,10 +158,21 @@ fn favicon_emitted_svg_only_no_binary_blobs() {158158     let text = String::from_utf8(raw).expect("favicon is valid UTF-8 text");159159     assert!(text.contains("<svg"), "favicon is an SVG document");160160     assert!(text.contains("viewBox"), "scales via viewBox");161-    // Mirrors the logo mark (same diamond-hub geometry, same palette).162-    let logo =163-        std::fs::read_to_string(manifest_dir().join("static").join("logo.svg")).expect("logo.svg");164-    assert_eq!(text, logo, "favicon adapts logo.svg exactly");161+    // Simplified mark-only 16px variant (#27): same diamond-hub palette162+    // as the lockup, but no letter <path>s and no <text>/fonts.163+    let fav_src = std::fs::read_to_string(manifest_dir().join("static").join("favicon.svg"))164+        .expect("favicon.svg");165+    assert_eq!(text, fav_src, "emitted favicon mirrors static source");166+    assert!(!text.contains("<text"), "favicon has no <text>");167+    assert!(!text.contains("<path"), "favicon is mark-only");168+    assert!(!text.to_lowercase().contains("font"), "no font refs");169+    // Header lockup (#27 + #28) is emitted too and carries the letters.170+    let logo_raw = std::fs::read(out.join("logo.svg")).expect("build emits logo.svg");171+    let logo = String::from_utf8(logo_raw).expect("logo is valid UTF-8 text");172+    assert!(logo.contains("<svg"), "logo is an SVG document");173+    assert!(logo.contains("<path"), "logo letters are <path>");174+    assert!(!logo.contains("<text"), "logo has no <text>");175+    assert_ne!(text, logo, "favicon is the simplified variant, not a copy");165176     // No binary icon formats anywhere in static/ or the build output.166177     for dir in [manifest_dir().join("static"), out.clone()] {167178         let mut stack = vec![dir.clone()];
tests/header_config.rs+59 −10
diff --git a/tests/header_config.rs b/tests/header_config.rsindex 374d829..5dc9ec2 100644--- a/tests/header_config.rs+++ b/tests/header_config.rs@@ -1,7 +1,8 @@1-//! Header config (m6, f0rge issues #21 + #24):1+//! Header config (m6, f0rge issues #21 + #24 + #27 + #28):22 //! `--repo-name`/`HU13_REPO_NAME` (flag > env > dirname, never blank) +33 //! `--description`/`HU13_DESCRIPTION` (flag > env > hidden, no empty `<p>`),4-//! threaded into header brand, `<title>`, og tags, and feed title/subtitle.4+//! threaded into `<title>`, og tags, feed title/subtitle, and the code-page5+//! repo-head — never into the minimal header (logo • nav only).56 //! Hostile `<script>` input renders as escaped text everywhere.67 78 use std::path::{Path, PathBuf};@@ -68,6 +69,12 @@ fn read(rel: &Path, join: &str) -> String {6869     std::fs::read_to_string(rel.join(join)).expect(join)6970 }7071 72+fn header_of(html: &str) -> &str {73+    let start = html.find("<header").expect("header open");74+    let end = html.find("</header>").expect("header close");75+    &html[start..end]76+}77+7178 fn set_env(k: &str, v: Option<&str>) {7279     unsafe {7380         match v {@@ -93,9 +100,25 @@ fn repo_name_chain_flag_beats_env_beats_dirname() {93100         "env name in title"94101     );95102     assert!(96-        index.contains("<span class=\"brand\">Env Name</span>"),97-        "env name in brand"103+        index.contains("og:title\" content=\"Env Name\""),104+        "env name in og:title"98105     );106+    assert!(107+        index.contains("<div class=\"repo-head\"><h1>Env Name</h1>"),108+        "env name in code-page repo-head"109+    );110+    assert!(111+        read(&out, "feed.atom").contains("<title>Env Name</title>"),112+        "env name in feed title"113+    );114+    let header = header_of(&index).to_string();115+    assert!(116+        !header.contains("<span class=\"brand\">"),117+        "no brand node in header"118+    );119+    assert!(!header.contains("hu13-word"), "no word node in header");120+    assert_eq!(header.matches("•").count(), 1, "logo • nav only");121+    assert!(!header.contains("Env Name"), "header carries zero names");99122 100123     // Flag beats env.101124     set_env("HU13_REPO_NAME", Some("Env Name"));@@ -103,6 +126,10 @@ fn repo_name_chain_flag_beats_env_beats_dirname() {103126     let index = read(&out, "index.html");104127     assert!(index.contains("<title>Flag Name</title>"), "flag wins");105128     assert!(!index.contains("Env Name"), "env suppressed by flag");129+    assert!(130+        index.contains("og:title\" content=\"Flag Name\""),131+        "og:title follows flag"132+    );106133 107134     // Neither -> dirname fallback, never blank.108135     set_env("HU13_REPO_NAME", None);@@ -113,8 +140,12 @@ fn repo_name_chain_flag_beats_env_beats_dirname() {113140         "dirname fallback"114141     );115142     assert!(116-        index.contains(&format!("<span class=\"brand\">{dirname}</span>")),117-        "brand fallback"143+        index.contains(&format!("<div class=\"repo-head\"><h1>{dirname}</h1>")),144+        "repo-head fallback"145+    );146+    assert!(147+        !header_of(&index).contains(&dirname),148+        "header stays nameless"118149     );119150     assert!(!index.contains("<h1></h1>"), "header never blank");120151     set_env("HU13_REPO_NAME", None);@@ -178,11 +209,18 @@ fn hostile_name_and_description_escape_everywhere() {178209     }179210     assert!(!feed.contains("<script>alert"), "feed escapes too");180211 181-    // Escaped forms present in every surface.212+    // Escaped forms present in every naming surface; the header stays nameless.182213     let name_e = "Evil &lt;script&gt;alert(1)&lt;/script&gt; Hub";183214     let desc_e = "Desc &lt;script&gt;alert(2)&lt;/script&gt; &amp; &quot;quoted&quot;";184-    assert!(index.contains(name_e), "header/brand escapes name");185-    assert!(index.contains(desc_e), "header escapes description");215+    assert!(216+        !header_of(&index).contains(name_e),217+        "header carries zero names"218+    );219+    assert!(220+        !header_of(&index).contains("<span class=\"brand\">"),221+        "no brand node to escape into"222+    );223+    assert!(index.contains(desc_e), "body escapes description");186224     assert!(187225         index.contains(&format!("<title>{name_e}</title>")),188226         "title escapes name"@@ -195,7 +233,18 @@ fn hostile_name_and_description_escape_everywhere() {195233         index.contains(&format!("og:description\" content=\"{desc_e}\"")),196234         "og:description matches"197235     );198-    assert!(commits.contains(name_e), "subpage brand escapes name");236+    assert!(237+        commits.contains(&format!("<title>commit")),238+        "subpage title kept"239+    );240+    assert!(241+        !header_of(&commits).contains(name_e),242+        "subpage header stays nameless"243+    );244+    assert!(245+        index.contains(&format!("<div class=\"repo-head\"><h1>{name_e}</h1>")),246+        "code-page repo-head escapes name"247+    );199248     assert!(200249         feed.contains(&format!("<title>{name_e}</title>")),201250         "feed title escapes name"
tests/logo.rs+57 −3
diff --git a/tests/logo.rs b/tests/logo.rsindex 113ef4a..08884b5 100644--- a/tests/logo.rs+++ b/tests/logo.rs@@ -1,6 +1,7 @@1-//! Logo suite (m6, f0rge issue #15):2-//! `static/logo.svg` exists + valid XML + <2KB, wordmark variant,3-//! ASCII banner limits, `<pre>` rendering invariant.1+//! Logo suite (m6, f0rge issues #15 + #27 + #28):2+//! `static/logo.svg` (lockup with hu13 as <path>) + `static/favicon.svg`3+//! (simplified mark-only 16px variant) exist + valid XML + <2KB total,4+//! wordmark variant, ASCII banner limits, `<pre>` rendering invariant.45 //!56 //! File-level suite (hu13 is a binary crate, so `src/logo.rs` unit tests67 //! cover the `ascii_pre()` helper itself; here we assert on disk).@@ -138,6 +139,59 @@ fn logo_svg_exists_valid_tiny() {138139         assert!(svg.contains(color), "Tokyo Night palette keeps {color}");139140     }140141     assert_sharp_no_gradient("logo.svg", &svg);142+    // Logo-text (#27): hu13 lettering is hand-written <path>, never143+    // <text>/fonts, so the header lockup has no font dependency.144+    assert!(svg.contains("<path"), "logo letters are <path>");145+    assert!(!svg.contains("<text"), "logo has no <text>");146+    assert!(147+        !svg.to_lowercase().contains("font"),148+        "logo has no font refs"149+    );150+    assert!(151+        svg.matches("<path").count() >= 2,152+        "logo carries mark + letter paths"153+    );154+}155+156+#[test]157+fn favicon_variant_is_simplified_mark_only() {158+    // Logo-text (#27): the 16px favicon is the simplified mark-only159+    // variant (no letters) so it stays legible; full lettering lives in160+    // logo.svg. Both stay valid XML, sharp, tiny, with <2KB combined.161+    let fav_path = manifest().join("static").join("favicon.svg");162+    let fav_len = std::fs::metadata(&fav_path)163+        .expect("static/favicon.svg exists")164+        .len();165+    assert!(166+        fav_len < 2048,167+        "favicon.svg is {fav_len} bytes, budget is 2KB"168+    );169+    let fav = read_static("favicon.svg");170+    check_well_formed(&fav);171+    assert!(fav.contains("<svg"), "favicon has an <svg> root");172+    assert!(fav.contains("viewBox"), "favicon scales via viewBox");173+    assert!(!fav.contains("<text"), "favicon has no <text>");174+    assert!(175+        !fav.to_lowercase().contains("font"),176+        "favicon has no font refs"177+    );178+    assert!(179+        !fav.contains("<path"),180+        "favicon is mark-only (no letter <path>s)"181+    );182+    assert_sharp_no_gradient("favicon.svg", &fav);183+    for color in ["#1a1b26", "#7aa2f7", "#7dcfff"] {184+        assert!(fav.contains(color), "favicon keeps {color}");185+    }186+    let logo_len = std::fs::metadata(manifest().join("static").join("logo.svg"))187+        .expect("static/logo.svg exists")188+        .len();189+    assert!(190+        logo_len + fav_len < 2048,191+        "logo ({logo_len}B) + favicon ({fav_len}B) stay under 2KB total"192+    );193+    let logo = read_static("logo.svg");194+    assert_ne!(logo, fav, "favicon is a simplified variant, not a copy");141195 }142196 143197 #[test]
tests/navbar.rs+30 −13
diff --git a/tests/navbar.rs b/tests/navbar.rsindex 701a9d9..bba644c 100644--- a/tests/navbar.rs+++ b/tests/navbar.rs@@ -1,5 +1,5 @@1-//! Navbar one-bar (m6, f0rge issue #18):2-//! single `<header>` (`logo hu13 • nav • repo`), scrollable nav strip1+//! Navbar one-bar (m6, f0rge issues #18 + #27 + #28):2+//! single `<header>` (logo-with-text • nav only), scrollable nav strip33 //! (own `nav-scroll` class: `overflow-x:auto` + nowrap, snap off, visible44 //! focus), logo img alt text, `aria-current` on the active tab.55 //!@@ -72,7 +72,7 @@ fn css() -> String {7272 }7373 7474 #[test]75-fn one_header_bar_with_logo_seps_brand_and_scroll_nav() {75+fn one_header_bar_with_logo_sep_and_scroll_nav() {7676     let out = build_fixture();7777     for page in ["index.html", "commits.html", "issues/index.html"] {7878         let html = std::fs::read_to_string(out.join(page)).expect(page);@@ -86,24 +86,28 @@ fn one_header_bar_with_logo_seps_brand_and_scroll_nav() {8686             html.contains("<nav"),8787             "{page} keeps a <nav> inside the header"8888         );89-        // Brand cluster: logo img (alt text) + hu13 • … • repo brand.89+        // Minimal lockup (#27 + #28): logo-with-text art • nav only.9090         assert!(91-            html.contains("<img class=\"site-logo\""),92-            "{page} shows the logo img"91+            html.contains("<img class=\"site-logo\" src=\"/logo.svg\""),92+            "{page} shows the logo lockup img"9393         );9494         assert!(9595             html.contains("alt=\"hu13 logo\""),9696             "{page} logo carries alt text"9797         );98-        assert!(html.contains(">hu13<"), "{page} names hu13");99-        assert!(html.contains("•"), "{page} has • separators");10098         assert!(101-            html.matches("•").count() >= 2,102-            "{page} separates brand/nav/repo with •"99+            !html.contains("hu13-word"),100+            "{page} drops the hu13 word node (letters live in logo.svg)"103101         );104102         assert!(105-            html.contains("<span class=\"brand\">"),106-            "{page} keeps the repo brand"103+            !html.contains("<span class=\"brand\">"),104+            "{page} drops the repo brand node"105+        );106+        let header = html.split("</header>").next().expect("header close");107+        assert_eq!(108+            header.matches("•").count(),109+            1,110+            "{page} header is logo • nav (exactly one sep)"107111         );108112         // Scrollable nav strip under its own class.109113         assert!(@@ -111,6 +115,20 @@ fn one_header_bar_with_logo_seps_brand_and_scroll_nav() {111115             "{page} nav has the scroll class"112116         );113117     }118+    // Repo name still lives outside the header (#28): title, og:title,119+    // code-page repo-head, and feed title.120+    let index = std::fs::read_to_string(out.join("index.html")).expect("index");121+    assert!(index.contains("<title>"), "title kept");122+    assert!(123+        index.contains("<meta property=\"og:title\""),124+        "og:title kept"125+    );126+    assert!(127+        index.contains("<div class=\"repo-head\"><h1>"),128+        "code-page repo-head title kept"129+    );130+    let feed = std::fs::read_to_string(out.join("feed.atom")).expect("feed");131+    assert!(feed.contains("<title>"), "feed title kept");114132 }115133 116134 #[test]117-- 2.55.0

Comments (0)

No comments yet.

Reviews (0)

No reviews yet.

all pulls