1 //! Light theme (m6, f0rge issue #19):2 //! CSS vars in `:root` (Tokyo Night dark) + light overrides under3 //! `prefers-color-scheme`; `color-scheme: dark light`; diff vars for #22.4 //! `theme-color` meta tags are owned by #16 (layout-level) — not asserted5 //! here to keep this change to `style.css` + tests only.6 7 use std::path::PathBuf;8 9 fn manifest() -> PathBuf {10 PathBuf::from(env!("CARGO_MANIFEST_DIR"))11 }12 13 fn css() -> String {14 std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css")15 }16 17 /// Strip `/* ... */` comments so palette hex in prose is not mistaken18 /// for hardcoded rule colors.19 fn without_comments(s: &str) -> String {20 let chars: Vec<char> = s.chars().collect();21 let mut out = String::with_capacity(s.len());22 let mut i = 0;23 while i < chars.len() {24 if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {25 i += 2;26 while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {27 i += 1;28 }29 i += 2;30 } else {31 out.push(chars[i]);32 i += 1;33 }34 }35 out36 }37 38 /// Remove every `:root { ... }` block (brace-matched) so var definitions39 /// may carry palette hex; what remains must reference vars, not hex.40 fn without_root_blocks(s: &str) -> String {41 let chars: Vec<char> = s.chars().collect();42 let mut out = String::with_capacity(s.len());43 let mut i = 0;44 while i < chars.len() {45 if chars[i..].iter().take(5).collect::<String>() == ":root" {46 if let Some(open) = chars[i..].iter().position(|c| *c == '{') {47 let mut depth = 0;48 let mut j = i + open;49 while j < chars.len() {50 if chars[j] == '{' {51 depth += 1;52 } else if chars[j] == '}' {53 depth -= 1;54 if depth == 0 {55 break;56 }57 }58 j += 1;59 }60 i = j + 1;61 continue;62 }63 }64 out.push(chars[i]);65 i += 1;66 }67 out68 }69 70 /// Inner text of the `@media (prefers-color-scheme:light)` block71 /// (brace-matched), so later file content cannot pollute assertions.72 fn light_block_inner(css: &str) -> &str {73 let at = css74 .find("@media (prefers-color-scheme:light)")75 .expect("light block");76 let open = css[at..].find('{').expect("light {") + at;77 let bytes = css.as_bytes();78 let mut depth = 0;79 let mut j = open;80 while j < bytes.len() {81 if bytes[j] == b'{' {82 depth += 1;83 } else if bytes[j] == b'}' {84 depth -= 1;85 if depth == 0 {86 break;87 }88 }89 j += 1;90 }91 &css[open + 1..j]92 }93 94 #[test]95 fn color_scheme_dark_light() {96 let css = css();97 assert!(98 css.contains("color-scheme:dark light"),99 ":root carries `color-scheme:dark light` so form controls follow the browser"100 );101 }102 103 #[test]104 fn dark_root_defines_vars() {105 let css = css();106 for var in [107 "--bg:#1a1b26",108 "--fg:#c0caf5",109 "--blue:#7aa2f7",110 "--cyan:#7dcfff",111 "--green:#9ece6a",112 "--red:#f7768e",113 "--border:#292e42",114 ] {115 assert!(css.contains(var), ":root defines dark {var}");116 }117 }118 119 #[test]120 fn light_media_block_overrides_vars() {121 let css = css();122 assert!(123 css.contains("@media (prefers-color-scheme:light)"),124 "light media block present (zero-JS OS switch)"125 );126 // Tokyo Night Light palette spot-checks inside the light block.127 let inner = light_block_inner(&css);128 for var in [129 "--bg:#f6f7f9",130 "--fg:#343b58",131 "--muted:#4a5273",132 "--surface:#ffffff",133 "--blue:#34548a",134 "--cyan:#0f4b6e",135 "--red:#8c4351",136 "--green:#33635c",137 ] {138 assert!(inner.contains(var), "light overrides {var}");139 }140 }141 142 #[test]143 fn light_block_has_no_duplicated_rule_bodies() {144 let css = css();145 let inner = light_block_inner(&css);146 // Light block must only redefine vars: no component selectors re-opened.147 for sel in [148 ".site-header",149 ".site-nav",150 "tr:nth-child",151 "pre{",152 "table{",153 ] {154 assert!(155 !inner.contains(sel),156 "light block must not duplicate rule bodies ({sel} found)"157 );158 }159 assert!(inner.contains(":root"), "light overrides live in :root");160 }161 162 #[test]163 fn no_hardcoded_palette_hex_outside_root() {164 let css = without_root_blocks(&without_comments(&css()));165 let palette = [166 "#1a1b26", "#16161e", "#c0caf5", "#565f89", "#7aa2f7", "#7dcfff", "#9ece6a", "#e0af68",167 "#f7768e", "#292e42", "#1f2335", "#9aa5ce", "#f6f7f9", "#343b58", "#34548a", "#0f4b6e",168 "#8c4351", "#33635c", "#eceef2", "#e4e7ee", "#5a4a78", "#7d5f33", "#ffffff", "#233328",169 "#38232e", "#c6d4ca", "#d9c2c7", "#d6dae2", "#4a5273", "#1f2335", "#24283b", "#bb9af7",170 "#ff9e64", "#7f87ad", "#2ac3de", "#8c6c3e", "#2a534d", "#7a3543", "#6a7590", "#8a93a5",171 ];172 for hex in palette {173 assert!(174 !css.to_lowercase().contains(hex),175 "no hardcoded {hex} outside :root var definitions (use var(--...))"176 );177 }178 }179 180 #[test]181 fn rules_reference_vars() {182 let css = css();183 for needle in [184 "background:var(--bg",185 "background:var(--bg-alt",186 "color:var(--fg",187 "color:var(--blue",188 "color:var(--cyan",189 "border:1px solid var(--border",190 ] {191 assert!(css.contains(needle), "rules reference {needle}");192 }193 }194 195 #[test]196 fn diff_vars_defined_dark_and_light() {197 let css = css();198 // Dark (current values) + light (best Tokyo Light pick; #22 may adjust).199 for var in [200 "--diff-add:#9ece6a",201 "--diff-del:#f7768e",202 "--diff-hunk:#7dcfff",203 "--diff-add:#2a534d",204 "--diff-del:#7a3543",205 "--diff-hunk:#0f4b6e",206 ] {207 assert!(css.contains(var), "diff var defined: {var}");208 }209 for var in ["--diff-add-bg", "--diff-del-bg", "--diff-file"] {210 assert!(css.contains(var), "diff var present: {var}");211 }212 // Forward-compat classes for the #22 renderer.213 for cls in [".diff-add", ".diff-del", ".diff-hunk", ".diff-file"] {214 assert!(css.contains(cls), "diff class styled: {cls}");215 }216 }217 218 #[test]219 fn light_budget_and_sharp_corners() {220 let path = manifest().join("static").join("style.css");221 let len = std::fs::metadata(&path).expect("style.css exists").len();222 assert!(223 len < 32768,224 "static/style.css is {len} bytes, budget is 32768 (see gl0w.md)"225 );226 }