1 From db32796c57a62ae7412d535a8d356d8c6d6c4c13 Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 07:39:27 +01004 Subject: [PATCH] m6/logo-text: vector lettering, header logo-only5 6 ---7 src/build.rs | 12 +++++---8 src/layout.rs | 56 +++++++++++++++++++++++-----------9 src/logo.rs | 43 ++++++++++++++++++++------10 static/favicon.svg | 1 +11 static/logo.svg | 13 +++++---12 tests/favicon.rs | 19 +++++++++---13 tests/header_config.rs | 69 ++++++++++++++++++++++++++++++++++++------14 tests/logo.rs | 60 ++++++++++++++++++++++++++++++++++--15 tests/navbar.rs | 42 +++++++++++++++++--------16 9 files changed, 251 insertions(+), 64 deletions(-)17 18 diff --git a/src/build.rs b/src/build.rs19 index 5083fa7..82eaf31 10064420 --- a/src/build.rs21 +++ b/src/build.rs22 @@ -370,11 +370,15 @@ fn write_style(out: &Path) -> Result<(), String> {23 std::fs::write(out.join("style.css"), css).map_err(|e| format!("hu13: write style.css: {e}"))24 }25 26 -/// Write `out/favicon.svg` + `out/manifest.webmanifest` from the embedded27 -/// static files (m6 favicon, f0rge issue #16). SVG-first, zero binary28 -/// blobs: the favicon is the hand-written logo mark, and the manifest29 -/// points at it (`icons` svg, `short_name` hu13, `display` minimal-ui).30 +/// Write `out/logo.svg` + `out/favicon.svg` + `out/manifest.webmanifest`31 +/// from the embedded static files (m6 favicon #16; logo-text #27 + #28).32 +/// SVG-first, zero binary blobs: the header lockup is `logo.svg`33 +/// (diamond-hub + hu13 as <path>, no <text>/fonts), the favicon is the34 +/// simplified mark-only variant for 16px, and the manifest points at the35 +/// favicon (`icons` svg, `short_name` hu13, `display` minimal-ui).36 fn write_favicon_and_manifest(out: &Path) -> Result<(), String> {37 + let logo = include_str!("../static/logo.svg");38 + std::fs::write(out.join("logo.svg"), logo).map_err(|e| format!("hu13: write logo.svg: {e}"))?;39 let icon = include_str!("../static/favicon.svg");40 std::fs::write(out.join("favicon.svg"), icon)41 .map_err(|e| format!("hu13: write favicon.svg: {e}"))?;42 diff --git a/src/layout.rs b/src/layout.rs43 index af31aa2..430c397 10064444 --- a/src/layout.rs45 +++ b/src/layout.rs46 @@ -87,12 +87,15 @@ pub fn page(title: &str, nav: &str, body: &str) -> String {47 }48 49 /// Render a full HTML page with site identity (m6 header config, f0rge50 -/// issues #21 + #24): the header brand shows `site_name`, `<title>` is51 +/// issues #21 + #24; minimal header f0rge issues #27 + #28): the header52 +/// carries zero names (logo-with-text art only + nav), `<title>` is53 /// the escaped page `title` (callers build it as `"{page} \u2014 {site}"`54 /// so the repo name is in every tab), `og:title` mirrors the page title,55 /// and `og:description` / `meta[name=description]` carry `description`56 -/// when present. `site_name` and `description` are escaped (hostile57 -/// `<script>` renders as text). `nav`/`body` are inlined as-is like [`page`].58 +/// when present. `site_name` still names the title/og surfaces (and the59 +/// feed title + code-page repo-head via `build`); it is never rendered in60 +/// the header. `description` is escaped (hostile `<script>` renders as61 +/// text). `nav`/`body` are inlined as-is like [`page`].62 ///63 /// Head standards (m6 favicon, f0rge issue #16) ride along on every page:64 /// SVG-first favicon (`rel=icon` + `apple-touch-icon` both point at65 @@ -192,10 +195,12 @@ pub fn page_site(66 description: Option<&str>,67 ) -> String {68 let title_e = esc(title);69 - let brand = esc(site_name);70 - // One-bar header (m6 navbar, f0rge issue #18): the active link (if71 - // any) gets `aria-current="page"` before the nav is inlined. The72 - // favicon doubles as the logo mark (byte-identical to `logo.svg`).73 + // One-bar minimal header (m6 navbar #18, logo-text #27 + #28): the74 + // active link (if any) gets `aria-current="page"` before the nav is75 + // inlined. Logo art is `logo.svg` (diamond-hub + hu13 as <path>, no76 + // <text>/fonts); `favicon.svg` is the simplified mark-only variant77 + // for 16px (see `logo.rs`). Header is logo + nav only: exactly one78 + // `•` separator, no `hu13-word` / `brand` name nodes.79 let mut head = format!(80 "<meta charset=\"utf-8\">\n\81 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\82 @@ -227,7 +232,7 @@ pub fn page_site(83 <html lang=\"en\">\n\84 <head>\n{head}</head>\n\85 <body>\n\86 -<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\87 +<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\88 <main class=\"wrap\">\n{body}\n</main>\n\89 </body>\n\90 </html>\n"91 @@ -301,25 +306,40 @@ mod tests {92 }93 94 #[test]95 - fn header_is_one_bar_with_logo_seps_brand_and_scroll_nav() {96 - // m6 navbar (f0rge issue #18): a single <header> holds logo +97 - // hu13 • scrollable nav • repo brand; no second top-level nav.98 + fn header_is_one_bar_with_logo_sep_and_scroll_nav() {99 + // m6 navbar (#18) + logo-text (#27 + #28): a single <header>100 + // holds logo-with-text • scrollable nav only; no hu13-word or101 + // brand name nodes. The repo name still lives in <title>/og.102 use super::{active_href, apply_active, page_site};103 let nav = "<a href=\"/\">code</a> · <a href=\"/commits.html\">commits</a>";104 let html = page_site("demo", nav, "<p>b</p>", "demo", None);105 assert_eq!(html.matches("<header").count(), 1, "single header bar");106 assert!(107 - html.contains("<img class=\"site-logo\" src=\"/favicon.svg\" alt=\"hu13 logo\""),108 - "logo img with alt text"109 + html.contains("<img class=\"site-logo\" src=\"/logo.svg\" alt=\"hu13 logo\""),110 + "logo lockup img with alt text"111 );112 assert!(113 - html.contains("<span class=\"hu13-word\">hu13</span>"),114 - "hu13 word"115 + !html.contains("hu13-word"),116 + "no hu13 word node (letters live in logo.svg paths)"117 );118 - assert!(html.matches("•").count() >= 2, "bullet separators");119 assert!(120 - html.contains("<span class=\"brand\">demo</span>"),121 - "repo brand kept"122 + !html.contains("<span class=\"brand\">"),123 + "no repo brand node in header"124 + );125 + let h0 = html.find("<header").expect("header open");126 + let h1 = html.find("</header>").expect("header close");127 + let header = &html[h0..h1];128 + assert!(!header.contains(">demo<"), "header carries zero names");129 + assert_eq!(130 + header.matches("•").count(),131 + 1,132 + "exactly one sep (logo • nav)"133 + );134 + // Repo name still lives in title + og:title.135 + assert!(html.contains("<title>demo</title>"), "title keeps name");136 + assert!(137 + html.contains("<meta property=\"og:title\" content=\"demo\">"),138 + "og:title keeps name"139 );140 assert!(html.contains("nav-scroll"), "nav has scroll class");141 assert!(html.contains("<nav"), "nav lives inside the header");142 diff --git a/src/logo.rs b/src/logo.rs143 index c822220..f580b2b 100644144 --- a/src/logo.rs145 +++ b/src/logo.rs146 @@ -1,12 +1,15 @@147 -//! hu13 logo + ASCII banner (m6, f0rge issue #15).148 +//! hu13 logo + ASCII banner (m6, f0rge issues #15 + #27 + #28).149 //!150 -//! The mark lives hand-written in `static/logo.svg` (square icon, reads at151 -//! 16px) plus `static/wordmark.svg` (horizontal lockup for the navbar); the152 -//! text banner lives in `static/ascii.txt`.153 +//! The lockup lives hand-written in `static/logo.svg` (diamond-hub mark +154 +//! hu13 lettered as <path>, no <text>/fonts; reads at 24px header size)155 +//! plus `static/wordmark.svg` (legacy horizontal lockup); the simplified156 +//! 16px favicon variant lives in `static/favicon.svg` (mark only, no157 +//! letters — small sizes stay legible, full lettering stays in logo.svg);158 +//! the text banner lives in `static/ascii.txt`.159 //!160 -//! [`ASCII_ART`], [`LOGO_SVG`] and [`WORDMARK_SVG`] re-expose those static161 -//! files for later issues (navbar #16, favicon #17, wiki README #19, agents162 -//! page) with no build step.163 +//! [`ASCII_ART`], [`LOGO_SVG`], [`FAVICON_SVG`] and [`WORDMARK_SVG`]164 +//! re-expose those static files for later issues (navbar #16, favicon #17,165 +//! wiki README #19, agents page) with no build step.166 //!167 //! [`ascii_pre`] renders the banner as an escaped `<pre>` block (see168 //! [`crate::layout::esc`]): the art itself is plain `|/-_` plus spaces so169 @@ -22,9 +25,14 @@ use crate::layout::esc;170 pub const ASCII_ART: &str = include_str!("../static/ascii.txt");171 172 #[allow(dead_code)]173 -/// Square mark, byte-for-byte `static/logo.svg`.174 +/// Header lockup, byte-for-byte `static/logo.svg` (mark + hu13 as <path>).175 pub const LOGO_SVG: &str = include_str!("../static/logo.svg");176 177 +#[allow(dead_code)]178 +/// Simplified 16px favicon variant, byte-for-byte `static/favicon.svg`179 +/// (mark only, no letters — documented choice for small-size legibility).180 +pub const FAVICON_SVG: &str = include_str!("../static/favicon.svg");181 +182 #[allow(dead_code)]183 /// Horizontal lockup, byte-for-byte `static/wordmark.svg`.184 pub const WORDMARK_SVG: &str = include_str!("../static/wordmark.svg");185 @@ -74,9 +82,26 @@ mod tests {186 187 #[test]188 fn svg_consts_match_tiny_marks() {189 - for (name, svg) in [("logo", LOGO_SVG), ("wordmark", WORDMARK_SVG)] {190 + for (name, svg) in [191 + ("logo", LOGO_SVG),192 + ("favicon", FAVICON_SVG),193 + ("wordmark", WORDMARK_SVG),194 + ] {195 assert!(svg.contains("<svg"), "{name} const holds svg");196 assert!(svg.len() < 2048, "{name} const stays tiny");197 }198 + // Logo-text (#27): lockup letters are <path>, never <text>/fonts.199 + assert!(LOGO_SVG.contains("<path"), "logo letters are paths");200 + assert!(!LOGO_SVG.contains("<text"), "logo has no <text>");201 + assert!(202 + !LOGO_SVG.to_lowercase().contains("font"),203 + "logo has no font refs"204 + );205 + // Simplified favicon variant (#27): mark only, no letters.206 + assert!(!FAVICON_SVG.contains("<text"), "favicon has no <text>");207 + assert!(208 + !FAVICON_SVG.contains("<path"),209 + "favicon is mark-only (no letter paths)"210 + );211 }212 }213 diff --git a/static/favicon.svg b/static/favicon.svg214 index 35a4426..098e978 100644215 --- a/static/favicon.svg216 +++ b/static/favicon.svg217 @@ -1,4 +1,5 @@218 <?xml version="1.0" encoding="UTF-8"?>219 +<!-- hu13 favicon: simplified mark-only variant for 16px legibility. Full lettering lives in logo.svg. -->220 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="hu13 logo">221 <title>hu13</title>222 <rect x="0" y="0" width="32" height="32" fill="#1a1b26"/>223 diff --git a/static/logo.svg b/static/logo.svg224 index 35a4426..1e7b01c 100644225 --- a/static/logo.svg226 +++ b/static/logo.svg227 @@ -1,8 +1,13 @@228 <?xml version="1.0" encoding="UTF-8"?>229 -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="hu13 logo">230 +<!-- hu13 lockup: diamond-hub mark + hu13 lettering as hand-drawn vectors. Reads at 24px header size. -->231 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 32" width="96" height="32" role="img" aria-label="hu13 logo">232 <title>hu13</title>233 - <rect x="0" y="0" width="32" height="32" fill="#1a1b26"/>234 - <rect x="0.5" y="0.5" width="31" height="31" fill="none" stroke="#292e42"/>235 - <polygon points="16,6 26,16 16,26 6,16" fill="none" stroke="#7aa2f7" stroke-width="3"/>236 + <rect x="0" y="0" width="96" height="32" fill="#1a1b26"/>237 + <rect x="0.5" y="0.5" width="95" height="31" fill="none" stroke="#292e42"/>238 + <polygon points="16,4 28,16 16,28 4,16" fill="none" stroke="#7aa2f7" stroke-width="3"/>239 <rect x="13" y="13" width="6" height="6" fill="#7dcfff"/>240 + <path d="M40 8 V24 M40 15 H46 V24" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>241 + <path d="M52 8 V21 H58 V8" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>242 + <path d="M64 10 L66 8 V24 M62 24 H70" fill="none" stroke="#c0caf5" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>243 + <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"/>244 </svg>245 diff --git a/tests/favicon.rs b/tests/favicon.rs246 index 4277749..3064d9f 100644247 --- a/tests/favicon.rs248 +++ b/tests/favicon.rs249 @@ -158,10 +158,21 @@ fn favicon_emitted_svg_only_no_binary_blobs() {250 let text = String::from_utf8(raw).expect("favicon is valid UTF-8 text");251 assert!(text.contains("<svg"), "favicon is an SVG document");252 assert!(text.contains("viewBox"), "scales via viewBox");253 - // Mirrors the logo mark (same diamond-hub geometry, same palette).254 - let logo =255 - std::fs::read_to_string(manifest_dir().join("static").join("logo.svg")).expect("logo.svg");256 - assert_eq!(text, logo, "favicon adapts logo.svg exactly");257 + // Simplified mark-only 16px variant (#27): same diamond-hub palette258 + // as the lockup, but no letter <path>s and no <text>/fonts.259 + let fav_src = std::fs::read_to_string(manifest_dir().join("static").join("favicon.svg"))260 + .expect("favicon.svg");261 + assert_eq!(text, fav_src, "emitted favicon mirrors static source");262 + assert!(!text.contains("<text"), "favicon has no <text>");263 + assert!(!text.contains("<path"), "favicon is mark-only");264 + assert!(!text.to_lowercase().contains("font"), "no font refs");265 + // Header lockup (#27 + #28) is emitted too and carries the letters.266 + let logo_raw = std::fs::read(out.join("logo.svg")).expect("build emits logo.svg");267 + let logo = String::from_utf8(logo_raw).expect("logo is valid UTF-8 text");268 + assert!(logo.contains("<svg"), "logo is an SVG document");269 + assert!(logo.contains("<path"), "logo letters are <path>");270 + assert!(!logo.contains("<text"), "logo has no <text>");271 + assert_ne!(text, logo, "favicon is the simplified variant, not a copy");272 // No binary icon formats anywhere in static/ or the build output.273 for dir in [manifest_dir().join("static"), out.clone()] {274 let mut stack = vec![dir.clone()];275 diff --git a/tests/header_config.rs b/tests/header_config.rs276 index 374d829..5dc9ec2 100644277 --- a/tests/header_config.rs278 +++ b/tests/header_config.rs279 @@ -1,7 +1,8 @@280 -//! Header config (m6, f0rge issues #21 + #24):281 +//! Header config (m6, f0rge issues #21 + #24 + #27 + #28):282 //! `--repo-name`/`HU13_REPO_NAME` (flag > env > dirname, never blank) +283 //! `--description`/`HU13_DESCRIPTION` (flag > env > hidden, no empty `<p>`),284 -//! threaded into header brand, `<title>`, og tags, and feed title/subtitle.285 +//! threaded into `<title>`, og tags, feed title/subtitle, and the code-page286 +//! repo-head — never into the minimal header (logo • nav only).287 //! Hostile `<script>` input renders as escaped text everywhere.288 289 use std::path::{Path, PathBuf};290 @@ -68,6 +69,12 @@ fn read(rel: &Path, join: &str) -> String {291 std::fs::read_to_string(rel.join(join)).expect(join)292 }293 294 +fn header_of(html: &str) -> &str {295 + let start = html.find("<header").expect("header open");296 + let end = html.find("</header>").expect("header close");297 + &html[start..end]298 +}299 +300 fn set_env(k: &str, v: Option<&str>) {301 unsafe {302 match v {303 @@ -93,9 +100,25 @@ fn repo_name_chain_flag_beats_env_beats_dirname() {304 "env name in title"305 );306 assert!(307 - index.contains("<span class=\"brand\">Env Name</span>"),308 - "env name in brand"309 + index.contains("og:title\" content=\"Env Name\""),310 + "env name in og:title"311 );312 + assert!(313 + index.contains("<div class=\"repo-head\"><h1>Env Name</h1>"),314 + "env name in code-page repo-head"315 + );316 + assert!(317 + read(&out, "feed.atom").contains("<title>Env Name</title>"),318 + "env name in feed title"319 + );320 + let header = header_of(&index).to_string();321 + assert!(322 + !header.contains("<span class=\"brand\">"),323 + "no brand node in header"324 + );325 + assert!(!header.contains("hu13-word"), "no word node in header");326 + assert_eq!(header.matches("•").count(), 1, "logo • nav only");327 + assert!(!header.contains("Env Name"), "header carries zero names");328 329 // Flag beats env.330 set_env("HU13_REPO_NAME", Some("Env Name"));331 @@ -103,6 +126,10 @@ fn repo_name_chain_flag_beats_env_beats_dirname() {332 let index = read(&out, "index.html");333 assert!(index.contains("<title>Flag Name</title>"), "flag wins");334 assert!(!index.contains("Env Name"), "env suppressed by flag");335 + assert!(336 + index.contains("og:title\" content=\"Flag Name\""),337 + "og:title follows flag"338 + );339 340 // Neither -> dirname fallback, never blank.341 set_env("HU13_REPO_NAME", None);342 @@ -113,8 +140,12 @@ fn repo_name_chain_flag_beats_env_beats_dirname() {343 "dirname fallback"344 );345 assert!(346 - index.contains(&format!("<span class=\"brand\">{dirname}</span>")),347 - "brand fallback"348 + index.contains(&format!("<div class=\"repo-head\"><h1>{dirname}</h1>")),349 + "repo-head fallback"350 + );351 + assert!(352 + !header_of(&index).contains(&dirname),353 + "header stays nameless"354 );355 assert!(!index.contains("<h1></h1>"), "header never blank");356 set_env("HU13_REPO_NAME", None);357 @@ -178,11 +209,18 @@ fn hostile_name_and_description_escape_everywhere() {358 }359 assert!(!feed.contains("<script>alert"), "feed escapes too");360 361 - // Escaped forms present in every surface.362 + // Escaped forms present in every naming surface; the header stays nameless.363 let name_e = "Evil <script>alert(1)</script> Hub";364 let desc_e = "Desc <script>alert(2)</script> & "quoted"";365 - assert!(index.contains(name_e), "header/brand escapes name");366 - assert!(index.contains(desc_e), "header escapes description");367 + assert!(368 + !header_of(&index).contains(name_e),369 + "header carries zero names"370 + );371 + assert!(372 + !header_of(&index).contains("<span class=\"brand\">"),373 + "no brand node to escape into"374 + );375 + assert!(index.contains(desc_e), "body escapes description");376 assert!(377 index.contains(&format!("<title>{name_e}</title>")),378 "title escapes name"379 @@ -195,7 +233,18 @@ fn hostile_name_and_description_escape_everywhere() {380 index.contains(&format!("og:description\" content=\"{desc_e}\"")),381 "og:description matches"382 );383 - assert!(commits.contains(name_e), "subpage brand escapes name");384 + assert!(385 + commits.contains(&format!("<title>commit")),386 + "subpage title kept"387 + );388 + assert!(389 + !header_of(&commits).contains(name_e),390 + "subpage header stays nameless"391 + );392 + assert!(393 + index.contains(&format!("<div class=\"repo-head\"><h1>{name_e}</h1>")),394 + "code-page repo-head escapes name"395 + );396 assert!(397 feed.contains(&format!("<title>{name_e}</title>")),398 "feed title escapes name"399 diff --git a/tests/logo.rs b/tests/logo.rs400 index 113ef4a..08884b5 100644401 --- a/tests/logo.rs402 +++ b/tests/logo.rs403 @@ -1,6 +1,7 @@404 -//! Logo suite (m6, f0rge issue #15):405 -//! `static/logo.svg` exists + valid XML + <2KB, wordmark variant,406 -//! ASCII banner limits, `<pre>` rendering invariant.407 +//! Logo suite (m6, f0rge issues #15 + #27 + #28):408 +//! `static/logo.svg` (lockup with hu13 as <path>) + `static/favicon.svg`409 +//! (simplified mark-only 16px variant) exist + valid XML + <2KB total,410 +//! wordmark variant, ASCII banner limits, `<pre>` rendering invariant.411 //!412 //! File-level suite (hu13 is a binary crate, so `src/logo.rs` unit tests413 //! cover the `ascii_pre()` helper itself; here we assert on disk).414 @@ -138,6 +139,59 @@ fn logo_svg_exists_valid_tiny() {415 assert!(svg.contains(color), "Tokyo Night palette keeps {color}");416 }417 assert_sharp_no_gradient("logo.svg", &svg);418 + // Logo-text (#27): hu13 lettering is hand-written <path>, never419 + // <text>/fonts, so the header lockup has no font dependency.420 + assert!(svg.contains("<path"), "logo letters are <path>");421 + assert!(!svg.contains("<text"), "logo has no <text>");422 + assert!(423 + !svg.to_lowercase().contains("font"),424 + "logo has no font refs"425 + );426 + assert!(427 + svg.matches("<path").count() >= 2,428 + "logo carries mark + letter paths"429 + );430 +}431 +432 +#[test]433 +fn favicon_variant_is_simplified_mark_only() {434 + // Logo-text (#27): the 16px favicon is the simplified mark-only435 + // variant (no letters) so it stays legible; full lettering lives in436 + // logo.svg. Both stay valid XML, sharp, tiny, with <2KB combined.437 + let fav_path = manifest().join("static").join("favicon.svg");438 + let fav_len = std::fs::metadata(&fav_path)439 + .expect("static/favicon.svg exists")440 + .len();441 + assert!(442 + fav_len < 2048,443 + "favicon.svg is {fav_len} bytes, budget is 2KB"444 + );445 + let fav = read_static("favicon.svg");446 + check_well_formed(&fav);447 + assert!(fav.contains("<svg"), "favicon has an <svg> root");448 + assert!(fav.contains("viewBox"), "favicon scales via viewBox");449 + assert!(!fav.contains("<text"), "favicon has no <text>");450 + assert!(451 + !fav.to_lowercase().contains("font"),452 + "favicon has no font refs"453 + );454 + assert!(455 + !fav.contains("<path"),456 + "favicon is mark-only (no letter <path>s)"457 + );458 + assert_sharp_no_gradient("favicon.svg", &fav);459 + for color in ["#1a1b26", "#7aa2f7", "#7dcfff"] {460 + assert!(fav.contains(color), "favicon keeps {color}");461 + }462 + let logo_len = std::fs::metadata(manifest().join("static").join("logo.svg"))463 + .expect("static/logo.svg exists")464 + .len();465 + assert!(466 + logo_len + fav_len < 2048,467 + "logo ({logo_len}B) + favicon ({fav_len}B) stay under 2KB total"468 + );469 + let logo = read_static("logo.svg");470 + assert_ne!(logo, fav, "favicon is a simplified variant, not a copy");471 }472 473 #[test]474 diff --git a/tests/navbar.rs b/tests/navbar.rs475 index 701a9d9..bba644c 100644476 --- a/tests/navbar.rs477 +++ b/tests/navbar.rs478 @@ -1,5 +1,5 @@479 -//! Navbar one-bar (m6, f0rge issue #18):480 -//! single `<header>` (`logo hu13 • nav • repo`), scrollable nav strip481 +//! Navbar one-bar (m6, f0rge issues #18 + #27 + #28):482 +//! single `<header>` (logo-with-text • nav only), scrollable nav strip483 //! (own `nav-scroll` class: `overflow-x:auto` + nowrap, snap off, visible484 //! focus), logo img alt text, `aria-current` on the active tab.485 //!486 @@ -72,7 +72,7 @@ fn css() -> String {487 }488 489 #[test]490 -fn one_header_bar_with_logo_seps_brand_and_scroll_nav() {491 +fn one_header_bar_with_logo_sep_and_scroll_nav() {492 let out = build_fixture();493 for page in ["index.html", "commits.html", "issues/index.html"] {494 let html = std::fs::read_to_string(out.join(page)).expect(page);495 @@ -86,24 +86,28 @@ fn one_header_bar_with_logo_seps_brand_and_scroll_nav() {496 html.contains("<nav"),497 "{page} keeps a <nav> inside the header"498 );499 - // Brand cluster: logo img (alt text) + hu13 • … • repo brand.500 + // Minimal lockup (#27 + #28): logo-with-text art • nav only.501 assert!(502 - html.contains("<img class=\"site-logo\""),503 - "{page} shows the logo img"504 + html.contains("<img class=\"site-logo\" src=\"/logo.svg\""),505 + "{page} shows the logo lockup img"506 );507 assert!(508 html.contains("alt=\"hu13 logo\""),509 "{page} logo carries alt text"510 );511 - assert!(html.contains(">hu13<"), "{page} names hu13");512 - assert!(html.contains("•"), "{page} has • separators");513 assert!(514 - html.matches("•").count() >= 2,515 - "{page} separates brand/nav/repo with •"516 + !html.contains("hu13-word"),517 + "{page} drops the hu13 word node (letters live in logo.svg)"518 );519 assert!(520 - html.contains("<span class=\"brand\">"),521 - "{page} keeps the repo brand"522 + !html.contains("<span class=\"brand\">"),523 + "{page} drops the repo brand node"524 + );525 + let header = html.split("</header>").next().expect("header close");526 + assert_eq!(527 + header.matches("•").count(),528 + 1,529 + "{page} header is logo • nav (exactly one sep)"530 );531 // Scrollable nav strip under its own class.532 assert!(533 @@ -111,6 +115,20 @@ fn one_header_bar_with_logo_seps_brand_and_scroll_nav() {534 "{page} nav has the scroll class"535 );536 }537 + // Repo name still lives outside the header (#28): title, og:title,538 + // code-page repo-head, and feed title.539 + let index = std::fs::read_to_string(out.join("index.html")).expect("index");540 + assert!(index.contains("<title>"), "title kept");541 + assert!(542 + index.contains("<meta property=\"og:title\""),543 + "og:title kept"544 + );545 + assert!(546 + index.contains("<div class=\"repo-head\"><h1>"),547 + "code-page repo-head title kept"548 + );549 + let feed = std::fs::read_to_string(out.join("feed.atom")).expect("feed");550 + assert!(feed.contains("<title>"), "feed title kept");551 }552 553 #[test]554 -- 555 2.55.0556