From c602a4bb5c5c90bdfb00c74d3589643b7335754c Mon Sep 17 00:00:00 2001 From: hu13-test Date: Tue, 27 Oct 2026 15:59:14 +0000 Subject: [PATCH] brand + theme foundations: gl0w lockup, AA palette, CSS budget (#69 #70 #76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #69: budget 15360 -> 32768 (CSS is 17.4KB with the new work) in the three tests and gl0w.md; add the token set the skin issues write against (--surface/-2, --fg-dim, spacing s1..s6, type scale, state colors, --radius) and document it at the top of the file. #70: header is now gl0w mark / hu13 lockup (96x32 logo keeps its aspect at 28px instead of being squeezed to 48x16); footer is a left-aligned brand block with 'hu13 — a gl0w project', a gl0w.world link and the provenance line; og:image (+width/height/ alt, twitter card, site_name) points at a generated 1200x630 PNG; logo.svg/favicon.svg lose the baked #1a1b26 slab and flip ink via prefers-color-scheme:light. #76: light palette is a real light surface set (#f6f7f9 page, #eceef2 header, #ffffff panels, #d6dae2 borders); --muted is now AA in both themes (#9aa5ce dark, #4a5273 light) and every text var is checked by tests/theme_contrast.rs (computed WCAG, both themes). Adds GFM table/task-list/del/footnote styles for #71. --- gl0w.md | 2 +- src/build.rs | 2 + src/layout.rs | 42 +++++++---- static/favicon.svg | 7 +- static/logo.svg | 17 +++-- static/og.png | Bin 0 -> 34764 bytes static/style.css | 80 +++++++++++++++++--- tests/brand.rs | 123 +++++++++++++++++++++++++++++++ tests/favicon.rs | 7 +- tests/light_theme.rs | 15 ++-- tests/logo.rs | 16 +++- tests/polish.rs | 4 +- tests/responsive.rs | 4 +- tests/theme_contrast.rs | 157 ++++++++++++++++++++++++++++++++++++++++ 14 files changed, 422 insertions(+), 54 deletions(-) create mode 100644 static/og.png create mode 100644 tests/brand.rs create mode 100644 tests/theme_contrast.rs diff --git a/gl0w.md b/gl0w.md index 9088ce8..0cb56f7 100644 --- a/gl0w.md +++ b/gl0w.md @@ -13,7 +13,7 @@ f0rge ci run # local CI: fmt, clippy, test, dogfood ``` Dogfood (stub-tolerant in m1): `cargo run -- build -o /tmp/hu13-dogfood` must exit 0. -CSS budget: `wc -c static/style.css` must stay under 15360 bytes (15KB); +CSS budget: `wc -c static/style.css` must stay under 32768 bytes (32KB); only allowed `border-radius` value is `0`: `grep border-radius static/style.css`. ## Inbox safety diff --git a/src/build.rs b/src/build.rs index a88adba..a49f34e 100644 --- a/src/build.rs +++ b/src/build.rs @@ -873,6 +873,8 @@ fn write_favicon_and_manifest(out: &Path) -> Result<(), String> { let icon = include_str!("../static/favicon.svg"); std::fs::write(out.join("favicon.svg"), icon) .map_err(|e| format!("hu13: write favicon.svg: {e}"))?; + let social = include_bytes!("../static/og.png"); + std::fs::write(out.join("og.png"), social).map_err(|e| format!("hu13: write og.png: {e}"))?; let manifest = include_str!("../static/manifest.webmanifest"); std::fs::write(out.join("manifest.webmanifest"), manifest) .map_err(|e| format!("hu13: write manifest.webmanifest: {e}"))?; diff --git a/src/layout.rs b/src/layout.rs index 70482a2..d985a04 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -100,7 +100,7 @@ pub fn page(title: &str, nav: &str, body: &str) -> String { /// Head standards (m6 favicon, f0rge issue #16) ride along on every page: /// SVG-first favicon (`rel=icon` + `apple-touch-icon` both point at /// `/favicon.svg` — no binary blobs in the repo), light/dark -/// `theme-color` (Tokyo Night `#d5d6db` / `#1a1b26`, matching +/// `theme-color` (Tokyo Night `#f6f7f9` / `#1a1b26`, matching /// `static/style.css`), `og:type=website`, and the `manifest.webmanifest` /// link.. /// Which nav `href` is active for a page (m6 navbar, f0rge issue #18). @@ -203,7 +203,7 @@ pub fn provenance_footer(branch: Option<&str>, short_sha: Option<&str>) -> Strin } } format!( - "

{line}

" + "" ) } @@ -268,13 +268,19 @@ fn page_site_inner( \n\ \n\ \n\ -\n\ +\n\ \n\ \n\ \n\ {title_e}\n\ \n\ -\n" +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n" ); if let Some(d) = description { let d_e = esc(d); @@ -293,7 +299,7 @@ fn page_site_inner( \n\ \n{head}\n\ \n\ -
\"hu13•
\n\ +
gl0w/\"hu13•
\n\
\n{body}\n
\n\ {footer}\n\ \n\ @@ -357,7 +363,7 @@ mod tests { for needle in [ "", "", - "", + "", "", "", @@ -612,21 +618,27 @@ mod tests { css.contains("main>h1:first-child") && css.contains("margin-top:var(--gap-head)"), "bare h1 shares nav->head gap" ); - // #51: 48px logo (was 24px / 1.5rem), vector kept, header centers. + // #70: the 96x32 lockup keeps its 3:1 aspect instead of being + // squeezed into a 48x48 square (lettering was illegible). assert!(css.contains(".site-logo"), "logo rule"); - assert!(css.contains("width:3rem"), "logo 48px width"); - assert!(css.contains("height:3rem"), "logo 48px height"); - assert!(!css.contains("width:1.5rem"), "old 24px gone"); + assert!( + css.contains("width:auto;height:1.75rem"), + "logo keeps its aspect at 28px tall" + ); + assert!(!css.contains("width:3rem"), "old square box gone"); let html = page("t", "", "

b

"); assert!( - html.contains("width=\"48\" height=\"48\""), - "header img 48px attrs" + html.contains("width=\"84\" height=\"28\""), + "header img 3:1 attrs" ); - // #52: footer provenance sits right, both themes (pure CSS var block). + // #70: footer is a left-aligned brand block (gl0w + hu13), not a + // lone right-aligned provenance line. assert!( - css.contains(".footer-provenance") && css.contains("text-align:right"), - "footer right-aligned" + css.contains(".footer-provenance") && css.contains("text-align:left"), + "footer aligns with content" ); + assert!(css.contains(".footer-brand"), "footer brand row styled"); + assert!(css.contains(".gl0w-mark"), "gl0w mark styled"); } #[test] diff --git a/static/favicon.svg b/static/favicon.svg index 098e978..d1572be 100644 --- a/static/favicon.svg +++ b/static/favicon.svg @@ -1,9 +1,8 @@ - + hu13 - - - + diff --git a/static/logo.svg b/static/logo.svg index 1e7b01c..63ed209 100644 --- a/static/logo.svg +++ b/static/logo.svg @@ -1,13 +1,16 @@ - + hu13 - - + - - - - + + + + + + diff --git a/static/og.png b/static/og.png new file mode 100644 index 0000000000000000000000000000000000000000..d7be379fba1e6798e29e3fdd0fe103d44b288c4a GIT binary patch literal 34764 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz*Hu{#=yWJe_^>i0|SFXvPY0F14ES>14Ba# z1H&%{28MC#lbHR;)fkCyzHKHUXu_V2pG!-aYr8%pgq{3f>enAoBl-d9#^{C;oujcP{G z-4TJBms}jJIL>n`yjGSWMqJX zgmodhRZunqLqnHW11pRdpu{K!V>@)RM8Mb#szM2H{Zl+fHH?PRXwra%z-WFLEj31q z%F+5^v=)Vxh{L7npfhFG*WAtLilo)5-w?ZuK{jU)qemIxh@I?X-5G!@}(g<|Ih*QMX053imt1XuVWnq$L?DPFwm>%2N( z3FyFor2~tCX2w6f>d(0mX5SA>CLOKT``Rcb+N&K{294`lUZ@J28M=#BE<#bb;tos16z!?1 z$gXjCs&Zga%!)NAuF(=_6l;x&IE3uy5I!ayu8kW|4ZOh;p%ZXu^?Y@>t5?imiO>-^ zwEDgqJY)hka%}h!%emf58KFvoCE|}>OZew@0dA<*7SsqQ7{+t1uaigk_LFDB(Hg}) zQ3fsWM(xvXhVDwGJz8x@8U+#pg&v0fRDc_=F^!>nr_vs+`3QYS1rk2a=2)-A29MbT zO{xbT)eFUi`XDT1b#6F%UMMb<4xY@ufRN`B`~`o(S@kuy

QzVnp2f9Xrtv(T=2 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 E)|q6T-90&W|DDH;hwuB}vb&Wxzb{(U_UZfl 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 Utu>RRzJToUboFyt=akR{08%c}VE_OC literal 0 HcmV?d00001 diff --git a/static/style.css b/static/style.css index a75e481..d764077 100644 --- a/static/style.css +++ b/static/style.css @@ -6,8 +6,11 @@ --bg:#1a1b26; --bg-alt:#16161e; --bg-th:#1f2335; + --surface:#1f2335; + --surface-2:#24283b; --fg:#c0caf5; - --muted:#565f89; + --fg-dim:#9aa5ce; + --muted:#9aa5ce; --blue:#7aa2f7; --cyan:#7dcfff; --green:#9ece6a; @@ -16,6 +19,28 @@ --border:#292e42; --visited:#9aa5ce; --btn-fg:#16161e; + --state-open:#9ece6a; + --state-merged:#bb9af7; + --state-closed:#f7768e; + /* syntax tokens (build-time highlighter) */ + --tok-kw:#bb9af7; + --tok-str:#9ece6a; + --tok-num:#ff9e64; + --tok-com:#8b93b8; + --tok-fn:#7aa2f7; + --tok-type:#2ac3de; + /* scale: one source for gaps, type, corners (f0rge #69) */ + --s1:.25rem; + --s2:.5rem; + --s3:.75rem; + --s4:1rem; + --s5:1.5rem; + --s6:2rem; + --fs-sm:.85rem; + --fs-lg:1.15rem; + --fs-xl:1.5rem; + --fs-2xl:2rem; + --radius:0; --gap-head:.75rem; --gap-content:1rem; --diff-add:#9ece6a; @@ -27,19 +52,31 @@ } @media (prefers-color-scheme:light){ :root{ - --bg:#d5d6db; - --bg-alt:#cbccd3; - --bg-th:#b4b8c7; + --bg:#f6f7f9; + --bg-alt:#eceef2; + --bg-th:#e4e7ee; + --surface:#ffffff; + --surface-2:#eef0f4; --fg:#343b58; - --muted:#565f89; + --fg-dim:#4a5273; + --muted:#4a5273; --blue:#34548a; --cyan:#0f4b6e; --green:#33635c; - --yellow:#8c6c3e; + --yellow:#7d5f33; --red:#8c4351; - --border:#b4b8c7; + --border:#d6dae2; --visited:#5a4a78; - --btn-fg:#e9e9ed; + --btn-fg:#ffffff; + --state-open:#33635c; + --state-merged:#5a4a78; + --state-closed:#8c4351; + --tok-kw:#5a4a78; + --tok-str:#33635c; + --tok-num:#7d5f33; + --tok-com:#4a5273; + --tok-fn:#34548a; + --tok-type:#0f4b6e; --diff-add:#33635c; --diff-add-bg:#c6d4ca; --diff-del:#8c4351; @@ -121,11 +158,26 @@ button:hover,input[type=submit]:hover{background:var(--cyan)} .btn-new:visited{color:var(--btn-fg)} .btn-new:hover,.btn-new:focus{background:var(--cyan);text-decoration:none} .muted{color:var(--muted)} -footer.site-footer{border-top:1px solid var(--border)} -footer.site-footer .wrap{padding:1rem} +/* GFM markdown (f0rge #71): task lists, strikethrough, footnotes. */ +.task-list-item{list-style:none} +.task-list-item input[type=checkbox]{margin:0 .4em 0 -1.2em;vertical-align:middle;accent-color:var(--green)} +.prose del,.del{color:var(--fg-dim);text-decoration:line-through} +.footnotes{margin-top:var(--s5);font-size:var(--fs-sm);color:var(--fg-dim)} +.footnotes ol{padding-left:var(--s5)} +.markdown table{background:var(--surface)} +.markdown th{background:var(--bg-th);font-weight:600} +.markdown td,.markdown th{padding:var(--s2) var(--s3)} +.markdown tr:nth-child(even) td{background:var(--bg-alt)} +footer.site-footer{border-top:1px solid var(--border);margin-top:var(--s6)} +footer.site-footer .wrap{padding:var(--s4) 1rem var(--s5)} +.footer-brand{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--s2) var(--s3);margin:0 0 var(--s2)} +.footer-brand .gl0w-mark{text-decoration:none} +.footer-tag{color:var(--fg);font-weight:600} +.footer-links{margin:0 0 var(--s2);font-size:var(--fs-sm)} +.footer-links a{margin-right:var(--s3)} /* Provenance line (m6/footer-line, f0rge #41): the single muted line inside the existing footer block — small, no extra whitespace. */ -footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem;text-align:right} +footer.site-footer .footer-provenance{margin:0;font-size:var(--fs-sm);text-align:left} /* Navbar one-bar (m6/navbar, f0rge #18): logo hu13 • scrollable nav • repo. Own classes only (.header-bar/.brand-lockup/.site-logo/.hu13-word/.sep/ .nav-scroll/.nav-scroll-row); earlier rules above are untouched. The nav @@ -134,7 +186,11 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem;text-align .site-header .wrap.header-bar{display:flex;flex-wrap:nowrap;align-items:center;gap:.5rem .75rem} .brand-lockup{display:inline-flex;align-items:center;gap:.5rem;flex:none;color:var(--fg);text-decoration:none;min-height:40px} .brand-lockup:visited{color:var(--fg)} -.site-logo{display:block;width:3rem;height:3rem;flex:none} +.gl0w-mark{font:600 .8rem/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.18em; + text-transform:lowercase;color:var(--cyan);border:1px solid var(--border);padding:.35rem .45rem} +.brand-lockup:hover .gl0w-mark{color:var(--blue);border-color:var(--blue)} +.lockup-slash{color:var(--border);font-weight:400} +.site-logo{display:block;width:auto;height:1.75rem;flex:none} .hu13-word{font-weight:700;color:var(--blue)} .sep{flex:none;color:var(--muted)} .site-header .site-nav.nav-scroll{flex:1 1 auto;min-width:0;background:none;border-bottom:none;overflow-x:auto;scroll-snap-type:none;-webkit-overflow-scrolling:touch} diff --git a/tests/brand.rs b/tests/brand.rs new file mode 100644 index 0000000..d81c023 --- /dev/null +++ b/tests/brand.rs @@ -0,0 +1,123 @@ +//! f0rge #70: gl0w branding + gl0w.world attribution on every page, +//! theme-aware logo, and an og:image social card. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn fresh(prefix: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "hu13-brand-{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:?}"); +} + +fn build() -> PathBuf { + let repo = fresh("repo"); + let out = fresh("out"); + git(&repo, &["init", "-b", "main"]); + git(&repo, &["config", "user.name", "hu13-test"]); + git(&repo, &["config", "user.email", "hu13@test.local"]); + git(&repo, &["config", "commit.gpgsign", "false"]); + std::fs::write(repo.join("README.md"), "# brand\n").unwrap(); + git(&repo, &["add", "-A"]); + git(&repo, &["commit", "-m", "brand"]); + let produced = Command::new(env!("CARGO_BIN_EXE_hu13")) + .arg("build") + .arg(&repo) + .arg("-o") + .arg(&out) + .output() + .unwrap(); + assert!( + produced.status.success(), + "{}", + String::from_utf8_lossy(&produced.stderr) + ); + out +} + +#[test] +fn header_and_footer_carry_gl0w_branding() { + let out = build(); + for page in ["index.html", "agents.html", "issues/index.html"] { + let html = std::fs::read_to_string(out.join(page)).expect(page); + assert!( + html.contains("", + "", + "", + "", + "", + ] { + assert!(html.contains(needle), "index.html has {needle}"); + } + let png = std::fs::read(out.join("og.png")).expect("og.png emitted"); + assert!(png.starts_with(b"\x89PNG\r\n\x1a\n"), "real PNG"); + assert!(png.len() > 1000, "not a stub"); +} + +#[test] +fn logo_has_no_baked_background() { + let logo = + std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("static/logo.svg")) + .unwrap(); + assert!(!logo.contains("fill=\"#1a1b26\""), "no dark slab"); + assert!( + logo.contains("@media (prefers-color-scheme:light)"), + "ink flips for light theme" + ); + let css = + std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("static/style.css")) + .unwrap(); + assert!( + css.contains(".site-logo{display:block;width:auto;height:1.75rem"), + "logo keeps its 3:1 aspect (was squeezed into 48x48)" + ); +} diff --git a/tests/favicon.rs b/tests/favicon.rs index 61c916e..cb85086 100644 --- a/tests/favicon.rs +++ b/tests/favicon.rs @@ -80,7 +80,7 @@ fn build_fixture() -> (PathBuf, PathBuf) { const HEAD_TAGS: &[&str] = &[ "", "", - "", + "", "", "", @@ -192,9 +192,10 @@ fn favicon_emitted_svg_only_no_binary_blobs() { .extension() .map(|e| e.to_string_lossy().to_ascii_lowercase()) .unwrap_or_default(); + let is_social_card = p.file_name().is_some_and(|n| n == "og.png"); assert!( - !["ico", "png", "icns"].contains(&ext.as_str()), - "no binary icon blobs: {}", + !["ico", "png", "icns"].contains(&ext.as_str()) || is_social_card, + "no binary icon blobs (social card excepted): {}", p.display() ); } diff --git a/tests/light_theme.rs b/tests/light_theme.rs index e403ef7..d7cb27c 100644 --- a/tests/light_theme.rs +++ b/tests/light_theme.rs @@ -126,8 +126,10 @@ fn light_media_block_overrides_vars() { // Tokyo Night Light palette spot-checks inside the light block. let inner = light_block_inner(&css); for var in [ - "--bg:#d5d6db", + "--bg:#f6f7f9", "--fg:#343b58", + "--muted:#4a5273", + "--surface:#ffffff", "--blue:#34548a", "--cyan:#0f4b6e", "--red:#8c4351", @@ -162,9 +164,10 @@ fn no_hardcoded_palette_hex_outside_root() { let css = without_root_blocks(&without_comments(&css())); let palette = [ "#1a1b26", "#16161e", "#c0caf5", "#565f89", "#7aa2f7", "#7dcfff", "#9ece6a", "#e0af68", - "#f7768e", "#292e42", "#1f2335", "#9aa5ce", "#d5d6db", "#343b58", "#34548a", "#0f4b6e", - "#8c4351", "#33635c", "#cbccd3", "#b4b8c7", "#5a4a78", "#8c6c3e", "#e9e9ed", "#233328", - "#38232e", "#c6d4ca", "#d9c2c7", + "#f7768e", "#292e42", "#1f2335", "#9aa5ce", "#f6f7f9", "#343b58", "#34548a", "#0f4b6e", + "#8c4351", "#33635c", "#eceef2", "#e4e7ee", "#5a4a78", "#7d5f33", "#ffffff", "#233328", + "#38232e", "#c6d4ca", "#d9c2c7", "#d6dae2", "#4a5273", "#1f2335", "#24283b", "#bb9af7", + "#ff9e64", "#7f87ad", "#2ac3de", "#8c6c3e", ]; for hex in palette { assert!( @@ -217,7 +220,7 @@ fn light_budget_and_sharp_corners() { let path = manifest().join("static").join("style.css"); let len = std::fs::metadata(&path).expect("style.css exists").len(); assert!( - len < 15360, - "static/style.css is {len} bytes, budget is 15360 (see gl0w.md)" + len < 32768, + "static/style.css is {len} bytes, budget is 32768 (see gl0w.md)" ); } diff --git a/tests/logo.rs b/tests/logo.rs index 21c3518..12537d0 100644 --- a/tests/logo.rs +++ b/tests/logo.rs @@ -137,9 +137,17 @@ fn logo_svg_exists_valid_tiny() { "svg namespace present" ); assert!(svg.contains("viewBox"), "scales via viewBox (16px + 32px)"); - for color in ["#1a1b26", "#7aa2f7", "#7dcfff"] { + for color in ["#7aa2f7", "#7dcfff", "#c0caf5"] { assert!(svg.contains(color), "Tokyo Night palette keeps {color}"); } + assert!( + !svg.contains("fill=\"#1a1b26\""), + "logo has no baked-in background (light theme must not show a dark slab)" + ); + assert!( + svg.contains("prefers-color-scheme:light"), + "lettering flips ink on the light theme" + ); assert_sharp_no_gradient("logo.svg", &svg); // Logo-text (#27): hu13 lettering is hand-written , never // /fonts, so the header lockup has no font dependency. @@ -182,9 +190,13 @@ fn favicon_variant_is_simplified_mark_only() { "favicon is mark-only (no letter s)" ); assert_sharp_no_gradient("favicon.svg", &fav); - for color in ["#1a1b26", "#7aa2f7", "#7dcfff"] { + for color in ["#7aa2f7", "#7dcfff"] { assert!(fav.contains(color), "favicon keeps {color}"); } + assert!( + !fav.contains("fill=\"#1a1b26\""), + "favicon has no baked-in background" + ); let logo_len = std::fs::metadata(manifest().join("static").join("logo.svg")) .expect("static/logo.svg exists") .len(); diff --git a/tests/polish.rs b/tests/polish.rs index 6e95324..ea48bc3 100644 --- a/tests/polish.rs +++ b/tests/polish.rs @@ -129,8 +129,8 @@ fn css_budget_under_15k() { let css = manifest().join("static").join("style.css"); let len = std::fs::metadata(&css).expect("style.css exists").len(); assert!( - len < 15360, - "static/style.css is {len} bytes, budget is 15360 (see gl0w.md)" + len < 32768, + "static/style.css is {len} bytes, budget is 32768 (see gl0w.md)" ); } diff --git a/tests/responsive.rs b/tests/responsive.rs index 1cf554a..bb72480 100644 --- a/tests/responsive.rs +++ b/tests/responsive.rs @@ -171,8 +171,8 @@ fn css_budget_and_sharp_corners() { let path = manifest().join("static").join("style.css"); let len = std::fs::metadata(&path).expect("style.css exists").len(); assert!( - len < 15360, - "static/style.css is {len} bytes, budget is 15360 (see gl0w.md)" + len < 32768, + "static/style.css is {len} bytes, budget is 32768 (see gl0w.md)" ); let css = css(); for line in css.lines() { diff --git a/tests/theme_contrast.rs b/tests/theme_contrast.rs new file mode 100644 index 0000000..9ce311f --- /dev/null +++ b/tests/theme_contrast.rs @@ -0,0 +1,157 @@ +//! f0rge #76: every text color must pass WCAG AA (4.5:1) in both themes. +//! Parsed from `static/style.css` so the palette cannot drift from the +//! rule set without this test noticing. + +use std::path::PathBuf; + +fn css() -> String { + let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("static/style.css"); + std::fs::read_to_string(p).expect("style.css") +} + +/// Body of a `{...}` block starting at `at` (brace-matched). +fn block(css: &str, at: usize) -> &str { + let open = css[at..].find('{').expect("open brace") + at; + let bytes = css.as_bytes(); + let mut depth = 0; + let mut i = open; + while i < bytes.len() { + match bytes[i] { + b'{' => depth += 1, + b'}' => { + depth -= 1; + if depth == 0 { + return &css[open + 1..i]; + } + } + _ => {} + } + i += 1; + } + panic!("unclosed block"); +} + +fn var(block: &str, name: &str) -> String { + let needle = format!("{name}:"); + let start = block + .find(&needle) + .unwrap_or_else(|| panic!("{name} missing")); + let rest = &block[start + needle.len()..]; + let end = rest.find([';', '}']).unwrap_or(rest.len()); + rest[..end].trim().to_string() +} + +fn channel(c: f64) -> f64 { + if c <= 0.039_28 { + c / 12.92 + } else { + ((c + 0.055) / 1.055).powf(2.4) + } +} + +fn lum(hex: &str) -> f64 { + let h = hex.trim_start_matches('#'); + let r = u8::from_str_radix(&h[0..2], 16).unwrap() as f64 / 255.0; + let g = u8::from_str_radix(&h[2..4], 16).unwrap() as f64 / 255.0; + let b = u8::from_str_radix(&h[4..6], 16).unwrap() as f64 / 255.0; + 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b) +} + +fn contrast(a: &str, b: &str) -> f64 { + let (hi, lo) = { + let (la, lb) = (lum(a), lum(b)); + if la > lb { + (la, lb) + } else { + (lb, la) + } + }; + (hi + 0.05) / (lo + 0.05) +} + +fn check(theme: &str, block: &str, pairs: &[(&str, &str)]) { + for (fg, bg) in pairs { + let f = var(block, fg); + let b = var(block, bg); + let ratio = contrast(&f, &b); + assert!( + ratio >= 4.5, + "{theme}: {fg} {f} on {bg} {b} = {ratio:.2}:1 (AA needs 4.5)" + ); + } +} + +#[test] +fn both_themes_pass_aa_for_text_colors() { + let css = css(); + let dark_at = css.find(":root{").expect("dark root"); + let dark = block(&css, dark_at); + let light_at = css + .find("@media (prefers-color-scheme:light)") + .expect("light block"); + let light = block(&css, light_at); + + let pairs = [ + ("--fg", "--bg"), + ("--fg", "--bg-alt"), + ("--fg", "--surface"), + ("--fg", "--surface-2"), + ("--fg-dim", "--bg"), + ("--fg-dim", "--bg-alt"), + ("--fg-dim", "--bg-th"), + ("--muted", "--bg"), + ("--muted", "--bg-th"), + ("--blue", "--bg"), + ("--blue", "--surface"), + ("--cyan", "--bg"), + ("--green", "--bg"), + ("--red", "--bg"), + ("--yellow", "--bg"), + ("--state-open", "--bg"), + ("--state-merged", "--bg"), + ("--state-closed", "--bg"), + ("--tok-kw", "--surface"), + ("--tok-str", "--surface"), + ("--tok-num", "--surface"), + ("--tok-com", "--surface"), + ("--tok-com", "--surface-2"), + ("--tok-fn", "--surface"), + ("--tok-type", "--surface"), + ]; + check("dark", dark, &pairs); + check("light", light, &pairs); +} + +#[test] +fn themes_define_the_same_vars() { + let css = css(); + let dark = block(&css, css.find(":root{").expect("dark root")); + let light = block( + &css, + css.find("@media (prefers-color-scheme:light)") + .expect("light block"), + ); + // Every dark color var has a light override (scale vars may be shared). + for line in dark.lines() { + let Some(name) = line + .trim() + .strip_prefix("--") + .and_then(|l| l.split(':').next()) + else { + continue; + }; + if name.starts_with("gap-") + || name.starts_with('s') + && name.len() <= 2 + && name[1..].chars().all(|c| c.is_ascii_digit()) + || name.starts_with("fs-") + || name == "radius" + { + continue; + } + assert!( + light.contains(&format!("--{name}:")), + "light theme must override --{name}" + ); + } +} -- 2.56.0