1 From a2638027ffb8ace1dc5c4af6d5143c6a735d1cda Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 06:49:09 +01004 Subject: [PATCH] m6/light-theme: css vars + tokyo night light (131 tests)5 6 ---7 static/style.css | 113 +++++++++++++++-------8 tests/light_theme.rs | 223 +++++++++++++++++++++++++++++++++++++++++++9 2 files changed, 299 insertions(+), 37 deletions(-)10 create mode 100644 tests/light_theme.rs11 12 diff --git a/static/style.css b/static/style.css13 index 32f05a8..443fc6a 10064414 --- a/static/style.css15 +++ b/static/style.css16 @@ -1,25 +1,67 @@17 /* hu13 — Tokyo Night dark-first, hand-written. Sharp corners only. Zero JS. */18 -/* Palette: Tokyo Night: bg #1a1b26, bg-alt #16161e, fg #c0caf5,19 - muted #565f89, blue #7aa2f7, cyan #7dcfff, green #9ece6a,20 - yellow #e0af68, red #f7768e, border #292e42. */21 +/* Vars: :root = Tokyo Night dark; light overrides under prefers-color-scheme. */22 *{box-sizing:border-box;border-radius:0}23 -:root{color-scheme:dark}24 -html{background:#1a1b26;-webkit-text-size-adjust:100%}25 -body{margin:0;background:#1a1b26;color:#c0caf5;26 +:root{27 + color-scheme:dark light;28 + --bg:#1a1b26;29 + --bg-alt:#16161e;30 + --bg-th:#1f2335;31 + --fg:#c0caf5;32 + --muted:#565f89;33 + --blue:#7aa2f7;34 + --cyan:#7dcfff;35 + --green:#9ece6a;36 + --yellow:#e0af68;37 + --red:#f7768e;38 + --border:#292e42;39 + --visited:#9aa5ce;40 + --btn-fg:#16161e;41 + --diff-add:#9ece6a;42 + --diff-add-bg:#233328;43 + --diff-del:#f7768e;44 + --diff-del-bg:#38232e;45 + --diff-hunk:#7dcfff;46 + --diff-file:#c0caf5;47 +}48 +@media (prefers-color-scheme:light){49 + :root{50 + --bg:#d5d6db;51 + --bg-alt:#cbccd3;52 + --bg-th:#b4b8c7;53 + --fg:#343b58;54 + --muted:#565f89;55 + --blue:#34548a;56 + --cyan:#0f4b6e;57 + --green:#33635c;58 + --yellow:#8c6c3e;59 + --red:#8c4351;60 + --border:#b4b8c7;61 + --visited:#5a4a78;62 + --btn-fg:#e9e9ed;63 + --diff-add:#33635c;64 + --diff-add-bg:#c6d4ca;65 + --diff-del:#8c4351;66 + --diff-del-bg:#d9c2c7;67 + --diff-hunk:#0f4b6e;68 + --diff-file:#343b58;69 + }70 +}71 +html{background:var(--bg);-webkit-text-size-adjust:100%}72 +body{margin:0;background:var(--bg);color:var(--fg);73 font-family:system-ui,-apple-system,"Segoe UI",Roboto,Ubuntu,Cantarell,sans-serif;74 line-height:1.55;font-size:16px;overflow-x:clip}75 img,video,svg{max-width:100%;height:auto}76 /* Fluid container: base = mobile single-column, side padding, never fixed px. */77 .wrap{max-width:100%;margin:0 auto;padding:0 1rem;min-width:0}78 -.site-header{background:#16161e;border-bottom:1px solid #292e42}79 +.site-header{background:var(--bg-alt);border-bottom:1px solid var(--border)}80 .site-header .wrap{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem 1rem;padding:.75rem 1rem}81 -.brand{font-weight:700;color:#7aa2f7;overflow-wrap:anywhere}82 -.site-nav{background:#16161e;border-bottom:1px solid #292e42}83 +.brand{font-weight:700;color:var(--blue);overflow-wrap:anywhere}84 +.site-nav{background:var(--bg-alt);border-bottom:1px solid var(--border)}85 .site-nav .wrap{display:flex;flex-wrap:wrap;gap:.25rem 1rem;padding:.5rem 1rem}86 -.site-nav a{display:inline-flex;align-items:center;min-height:40px;color:#7dcfff;text-decoration:none;padding:.35rem .1rem}87 +.site-nav a{display:inline-flex;align-items:center;min-height:40px;color:var(--cyan);text-decoration:none;padding:.35rem .1rem}88 .site-nav a:hover,.site-nav a:focus{text-decoration:underline}89 -a{color:#7aa2f7}90 -a:visited{color:#9aa5ce}91 +a{color:var(--blue)}92 +a:visited{color:var(--visited)}93 main{padding:1.25rem 0 3rem;min-width:0}94 /* Repo header stacks on narrow; clone cmd + sha wrap safely. */95 .repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0}96 @@ -27,20 +69,25 @@ main{padding:1.25rem 0 3rem;min-width:0}97 .clone,.repo-meta{margin:.25rem 0;min-width:0}98 .clone code,.repo-meta code{overflow-wrap:anywhere;word-break:break-all}99 code,kbd,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;100 - background:#16161e;color:#9ece6a;padding:.1em .3em;overflow-wrap:anywhere}101 -pre{background:#16161e;color:#c0caf5;border:1px solid #292e42;102 + background:var(--bg-alt);color:var(--green);padding:.1em .3em;overflow-wrap:anywhere}103 +pre{background:var(--bg-alt);color:var(--fg);border:1px solid var(--border);104 padding:1rem;overflow-x:auto;white-space:pre;line-height:1.5;max-width:100%}105 pre code{background:none;padding:0;color:inherit;overflow-wrap:normal;word-break:normal}106 +/* Diff colors (line-level spans for #22; renderer lands there, vars live here). */107 +.diff-add{color:var(--diff-add);background:var(--diff-add-bg)}108 +.diff-del{color:var(--diff-del);background:var(--diff-del-bg)}109 +.diff-hunk{color:var(--diff-hunk)}110 +.diff-file{color:var(--diff-file);font-weight:700}111 /* Scroll containers: tables never break layout; pre scrolls inside too. */112 .table-scroll,.pre-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}113 .table-scroll{margin:1rem 0}114 .pre-scroll{margin:1rem 0}115 .table-scroll table{margin:0}116 .pre-scroll pre{margin:0}117 -table{border-collapse:collapse;width:100%;margin:1rem 0;background:#16161e}118 -th,td{text-align:left;padding:.5rem .75rem;border:1px solid #292e42;vertical-align:top;overflow-wrap:anywhere}119 -th{background:#1f2335;color:#7dcfff}120 -tr:nth-child(even) td{background:#1a1b26}121 +table{border-collapse:collapse;width:100%;margin:1rem 0;background:var(--bg-alt)}122 +th,td{text-align:left;padding:.5rem .75rem;border:1px solid var(--border);vertical-align:top;overflow-wrap:anywhere}123 +th{background:var(--bg-th);color:var(--cyan)}124 +tr:nth-child(even) td{background:var(--bg)}125 td code,th code{word-break:break-all}126 /* List tables (m6/tables): long titles/shas wrap inside the scroll box; the # column stays narrow. */127 td.title,td.sha{overflow-wrap:anywhere}128 @@ -51,20 +98,20 @@ td.num{width:1%;white-space:nowrap}129 /* Forms: full-width on mobile, >=40px touch targets. */130 form{max-width:44rem;margin:1rem 0;min-width:0}131 label{display:block;margin:.75rem 0 .25rem;font-weight:600}132 -input[type=text],textarea{display:block;width:100%;max-width:100%;background:#16161e;133 - color:#c0caf5;border:1px solid #292e42;padding:.5rem .6rem;font:inherit;min-height:40px}134 -input[type=text]:focus,textarea:focus{outline:2px solid #7aa2f7;outline-offset:0}135 +input[type=text],textarea{display:block;width:100%;max-width:100%;background:var(--bg-alt);136 + color:var(--fg);border:1px solid var(--border);padding:.5rem .6rem;font:inherit;min-height:40px}137 +input[type=text]:focus,textarea:focus{outline:2px solid var(--blue);outline-offset:0}138 textarea{min-height:12rem}139 button,input[type=submit]{display:inline-flex;align-items:center;justify-content:center;140 - min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;border:1px solid #7aa2f7;141 + min-height:40px;min-width:40px;background:var(--blue);color:var(--btn-fg);border:1px solid var(--blue);142 padding:.5rem 1rem;font:inherit;font-weight:700;cursor:pointer}143 -button:hover,input[type=submit]:hover{background:#7dcfff}144 -.muted{color:#565f89}145 -.badge{display:inline-block;border:1px solid #292e42;padding:.1em .5em;font-size:.85em}146 -.badge.pass{color:#9ece6a}.badge.fail{color:#f7768e}147 -.badge.running{color:#e0af68}.badge.cancelled{color:#565f89}148 -.notice{border:1px solid #e0af68;padding:.75rem 1rem;margin:1rem 0;overflow-wrap:anywhere}149 -footer.site-footer{border-top:1px solid #292e42;color:#565f89}150 +button:hover,input[type=submit]:hover{background:var(--cyan)}151 +.muted{color:var(--muted)}152 +.badge{display:inline-block;border:1px solid var(--border);padding:.1em .5em;font-size:.85em}153 +.badge.pass{color:var(--green)}.badge.fail{color:var(--red)}154 +.badge.running{color:var(--yellow)}.badge.cancelled{color:var(--muted)}155 +.notice{border:1px solid var(--yellow);padding:.75rem 1rem;margin:1rem 0;overflow-wrap:anywhere}156 +footer.site-footer{border-top:1px solid var(--border);color:var(--muted)}157 footer.site-footer .wrap{padding:1rem}158 @media (min-width:768px){159 .cols{grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)}160 @@ -74,11 +121,3 @@ footer.site-footer .wrap{padding:1rem}161 @media (min-width:1100px){162 .wrap{max-width:72rem;padding:0 1.5rem}163 }164 -@media (prefers-color-scheme:light){165 - html,body{background:#d5d6db;color:#343b58}166 - .site-header,.site-nav,code,kbd,samp,pre,table,input[type=text],textarea{background:#cbccd3}167 - pre,pre code{color:#343b58}168 - th{background:#b4b8c7;color:#0f4b6e}169 - tr:nth-child(even) td{background:#d5d6db}170 - a{color:#34548a}171 -}172 diff --git a/tests/light_theme.rs b/tests/light_theme.rs173 new file mode 100644174 index 0000000..e403ef7175 --- /dev/null176 +++ b/tests/light_theme.rs177 @@ -0,0 +1,223 @@178 +//! Light theme (m6, f0rge issue #19):179 +//! CSS vars in `:root` (Tokyo Night dark) + light overrides under180 +//! `prefers-color-scheme`; `color-scheme: dark light`; diff vars for #22.181 +//! `theme-color` meta tags are owned by #16 (layout-level) — not asserted182 +//! here to keep this change to `style.css` + tests only.183 +184 +use std::path::PathBuf;185 +186 +fn manifest() -> PathBuf {187 + PathBuf::from(env!("CARGO_MANIFEST_DIR"))188 +}189 +190 +fn css() -> String {191 + std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css")192 +}193 +194 +/// Strip `/* ... */` comments so palette hex in prose is not mistaken195 +/// for hardcoded rule colors.196 +fn without_comments(s: &str) -> String {197 + let mut out = String::with_capacity(s.len());198 + let bytes = s.as_bytes();199 + let mut i = 0;200 + while i < bytes.len() {201 + if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {202 + i += 2;203 + while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {204 + i += 1;205 + }206 + i += 2;207 + } else {208 + out.push(bytes[i] as char);209 + i += 1;210 + }211 + }212 + out213 +}214 +215 +/// Remove every `:root { ... }` block (brace-matched) so var definitions216 +/// may carry palette hex; what remains must reference vars, not hex.217 +fn without_root_blocks(s: &str) -> String {218 + let mut out = String::with_capacity(s.len());219 + let bytes = s.as_bytes();220 + let mut i = 0;221 + while i < bytes.len() {222 + if s[i..].starts_with(":root") {223 + if let Some(open) = s[i..].find('{') {224 + let mut depth = 0;225 + let mut j = i + open;226 + while j < bytes.len() {227 + if bytes[j] == b'{' {228 + depth += 1;229 + } else if bytes[j] == b'}' {230 + depth -= 1;231 + if depth == 0 {232 + break;233 + }234 + }235 + j += 1;236 + }237 + i = j + 1;238 + continue;239 + }240 + }241 + out.push(bytes[i] as char);242 + i += 1;243 + }244 + out245 +}246 +247 +/// Inner text of the `@media (prefers-color-scheme:light)` block248 +/// (brace-matched), so later file content cannot pollute assertions.249 +fn light_block_inner(css: &str) -> &str {250 + let at = css251 + .find("@media (prefers-color-scheme:light)")252 + .expect("light block");253 + let open = css[at..].find('{').expect("light {") + at;254 + let bytes = css.as_bytes();255 + let mut depth = 0;256 + let mut j = open;257 + while j < bytes.len() {258 + if bytes[j] == b'{' {259 + depth += 1;260 + } else if bytes[j] == b'}' {261 + depth -= 1;262 + if depth == 0 {263 + break;264 + }265 + }266 + j += 1;267 + }268 + &css[open + 1..j]269 +}270 +271 +#[test]272 +fn color_scheme_dark_light() {273 + let css = css();274 + assert!(275 + css.contains("color-scheme:dark light"),276 + ":root carries `color-scheme:dark light` so form controls follow the browser"277 + );278 +}279 +280 +#[test]281 +fn dark_root_defines_vars() {282 + let css = css();283 + for var in [284 + "--bg:#1a1b26",285 + "--fg:#c0caf5",286 + "--blue:#7aa2f7",287 + "--cyan:#7dcfff",288 + "--green:#9ece6a",289 + "--red:#f7768e",290 + "--border:#292e42",291 + ] {292 + assert!(css.contains(var), ":root defines dark {var}");293 + }294 +}295 +296 +#[test]297 +fn light_media_block_overrides_vars() {298 + let css = css();299 + assert!(300 + css.contains("@media (prefers-color-scheme:light)"),301 + "light media block present (zero-JS OS switch)"302 + );303 + // Tokyo Night Light palette spot-checks inside the light block.304 + let inner = light_block_inner(&css);305 + for var in [306 + "--bg:#d5d6db",307 + "--fg:#343b58",308 + "--blue:#34548a",309 + "--cyan:#0f4b6e",310 + "--red:#8c4351",311 + "--green:#33635c",312 + ] {313 + assert!(inner.contains(var), "light overrides {var}");314 + }315 +}316 +317 +#[test]318 +fn light_block_has_no_duplicated_rule_bodies() {319 + let css = css();320 + let inner = light_block_inner(&css);321 + // Light block must only redefine vars: no component selectors re-opened.322 + for sel in [323 + ".site-header",324 + ".site-nav",325 + "tr:nth-child",326 + "pre{",327 + "table{",328 + ] {329 + assert!(330 + !inner.contains(sel),331 + "light block must not duplicate rule bodies ({sel} found)"332 + );333 + }334 + assert!(inner.contains(":root"), "light overrides live in :root");335 +}336 +337 +#[test]338 +fn no_hardcoded_palette_hex_outside_root() {339 + let css = without_root_blocks(&without_comments(&css()));340 + let palette = [341 + "#1a1b26", "#16161e", "#c0caf5", "#565f89", "#7aa2f7", "#7dcfff", "#9ece6a", "#e0af68",342 + "#f7768e", "#292e42", "#1f2335", "#9aa5ce", "#d5d6db", "#343b58", "#34548a", "#0f4b6e",343 + "#8c4351", "#33635c", "#cbccd3", "#b4b8c7", "#5a4a78", "#8c6c3e", "#e9e9ed", "#233328",344 + "#38232e", "#c6d4ca", "#d9c2c7",345 + ];346 + for hex in palette {347 + assert!(348 + !css.to_lowercase().contains(hex),349 + "no hardcoded {hex} outside :root var definitions (use var(--...))"350 + );351 + }352 +}353 +354 +#[test]355 +fn rules_reference_vars() {356 + let css = css();357 + for needle in [358 + "background:var(--bg",359 + "background:var(--bg-alt",360 + "color:var(--fg",361 + "color:var(--blue",362 + "color:var(--cyan",363 + "border:1px solid var(--border",364 + ] {365 + assert!(css.contains(needle), "rules reference {needle}");366 + }367 +}368 +369 +#[test]370 +fn diff_vars_defined_dark_and_light() {371 + let css = css();372 + // Dark (current values) + light (best Tokyo Light pick; #22 may adjust).373 + for var in [374 + "--diff-add:#9ece6a",375 + "--diff-del:#f7768e",376 + "--diff-hunk:#7dcfff",377 + "--diff-add:#33635c",378 + "--diff-del:#8c4351",379 + "--diff-hunk:#0f4b6e",380 + ] {381 + assert!(css.contains(var), "diff var defined: {var}");382 + }383 + for var in ["--diff-add-bg", "--diff-del-bg", "--diff-file"] {384 + assert!(css.contains(var), "diff var present: {var}");385 + }386 + // Forward-compat classes for the #22 renderer.387 + for cls in [".diff-add", ".diff-del", ".diff-hunk", ".diff-file"] {388 + assert!(css.contains(cls), "diff class styled: {cls}");389 + }390 +}391 +392 +#[test]393 +fn light_budget_and_sharp_corners() {394 + let path = manifest().join("static").join("style.css");395 + let len = std::fs::metadata(&path).expect("style.css exists").len();396 + assert!(397 + len < 15360,398 + "static/style.css is {len} bytes, budget is 15360 (see gl0w.md)"399 + );400 +}401 -- 402 2.55.0403