.forge/prs/1b1f409f-99e2-42b5-acc5-933940a4e63f/patch.diff 15.0 KiB 403 lines Raw
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