#17 m6/light-theme

mergedpassopened
details
branches
master ← m6/light-theme
updated
id
1b1f409f-99e2-42b5-acc5-933940a4e63f

Patch

static/style.css+76 −37
diff --git a/static/style.css b/static/style.cssindex 32f05a8..443fc6a 100644--- a/static/style.css+++ b/static/style.css@@ -1,25 +1,67 @@11 /* hu13 — Tokyo Night dark-first, hand-written. Sharp corners only. Zero JS. */2-/* Palette: Tokyo Night: bg #1a1b26, bg-alt #16161e, fg #c0caf5,3-   muted #565f89, blue #7aa2f7, cyan #7dcfff, green #9ece6a,4-   yellow #e0af68, red #f7768e, border #292e42. */2+/* Vars: :root = Tokyo Night dark; light overrides under prefers-color-scheme. */53 *{box-sizing:border-box;border-radius:0}6-:root{color-scheme:dark}7-html{background:#1a1b26;-webkit-text-size-adjust:100%}8-body{margin:0;background:#1a1b26;color:#c0caf5;4+:root{5+  color-scheme:dark light;6+  --bg:#1a1b26;7+  --bg-alt:#16161e;8+  --bg-th:#1f2335;9+  --fg:#c0caf5;10+  --muted:#565f89;11+  --blue:#7aa2f7;12+  --cyan:#7dcfff;13+  --green:#9ece6a;14+  --yellow:#e0af68;15+  --red:#f7768e;16+  --border:#292e42;17+  --visited:#9aa5ce;18+  --btn-fg:#16161e;19+  --diff-add:#9ece6a;20+  --diff-add-bg:#233328;21+  --diff-del:#f7768e;22+  --diff-del-bg:#38232e;23+  --diff-hunk:#7dcfff;24+  --diff-file:#c0caf5;25+}26+@media (prefers-color-scheme:light){27+  :root{28+    --bg:#d5d6db;29+    --bg-alt:#cbccd3;30+    --bg-th:#b4b8c7;31+    --fg:#343b58;32+    --muted:#565f89;33+    --blue:#34548a;34+    --cyan:#0f4b6e;35+    --green:#33635c;36+    --yellow:#8c6c3e;37+    --red:#8c4351;38+    --border:#b4b8c7;39+    --visited:#5a4a78;40+    --btn-fg:#e9e9ed;41+    --diff-add:#33635c;42+    --diff-add-bg:#c6d4ca;43+    --diff-del:#8c4351;44+    --diff-del-bg:#d9c2c7;45+    --diff-hunk:#0f4b6e;46+    --diff-file:#343b58;47+  }48+}49+html{background:var(--bg);-webkit-text-size-adjust:100%}50+body{margin:0;background:var(--bg);color:var(--fg);951   font-family:system-ui,-apple-system,"Segoe UI",Roboto,Ubuntu,Cantarell,sans-serif;1052   line-height:1.55;font-size:16px;overflow-x:clip}1153 img,video,svg{max-width:100%;height:auto}1254 /* Fluid container: base = mobile single-column, side padding, never fixed px. */1355 .wrap{max-width:100%;margin:0 auto;padding:0 1rem;min-width:0}14-.site-header{background:#16161e;border-bottom:1px solid #292e42}56+.site-header{background:var(--bg-alt);border-bottom:1px solid var(--border)}1557 .site-header .wrap{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem 1rem;padding:.75rem 1rem}16-.brand{font-weight:700;color:#7aa2f7;overflow-wrap:anywhere}17-.site-nav{background:#16161e;border-bottom:1px solid #292e42}58+.brand{font-weight:700;color:var(--blue);overflow-wrap:anywhere}59+.site-nav{background:var(--bg-alt);border-bottom:1px solid var(--border)}1860 .site-nav .wrap{display:flex;flex-wrap:wrap;gap:.25rem 1rem;padding:.5rem 1rem}19-.site-nav a{display:inline-flex;align-items:center;min-height:40px;color:#7dcfff;text-decoration:none;padding:.35rem .1rem}61+.site-nav a{display:inline-flex;align-items:center;min-height:40px;color:var(--cyan);text-decoration:none;padding:.35rem .1rem}2062 .site-nav a:hover,.site-nav a:focus{text-decoration:underline}21-a{color:#7aa2f7}22-a:visited{color:#9aa5ce}63+a{color:var(--blue)}64+a:visited{color:var(--visited)}2365 main{padding:1.25rem 0 3rem;min-width:0}2466 /* Repo header stacks on narrow; clone cmd + sha wrap safely. */2567 .repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0}@@ -27,20 +69,25 @@ main{padding:1.25rem 0 3rem;min-width:0}2769 .clone,.repo-meta{margin:.25rem 0;min-width:0}2870 .clone code,.repo-meta code{overflow-wrap:anywhere;word-break:break-all}2971 code,kbd,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;30-  background:#16161e;color:#9ece6a;padding:.1em .3em;overflow-wrap:anywhere}31-pre{background:#16161e;color:#c0caf5;border:1px solid #292e42;72+  background:var(--bg-alt);color:var(--green);padding:.1em .3em;overflow-wrap:anywhere}73+pre{background:var(--bg-alt);color:var(--fg);border:1px solid var(--border);3274   padding:1rem;overflow-x:auto;white-space:pre;line-height:1.5;max-width:100%}3375 pre code{background:none;padding:0;color:inherit;overflow-wrap:normal;word-break:normal}76+/* Diff colors (line-level spans for #22; renderer lands there, vars live here). */77+.diff-add{color:var(--diff-add);background:var(--diff-add-bg)}78+.diff-del{color:var(--diff-del);background:var(--diff-del-bg)}79+.diff-hunk{color:var(--diff-hunk)}80+.diff-file{color:var(--diff-file);font-weight:700}3481 /* Scroll containers: tables never break layout; pre scrolls inside too. */3582 .table-scroll,.pre-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}3683 .table-scroll{margin:1rem 0}3784 .pre-scroll{margin:1rem 0}3885 .table-scroll table{margin:0}3986 .pre-scroll pre{margin:0}40-table{border-collapse:collapse;width:100%;margin:1rem 0;background:#16161e}41-th,td{text-align:left;padding:.5rem .75rem;border:1px solid #292e42;vertical-align:top;overflow-wrap:anywhere}42-th{background:#1f2335;color:#7dcfff}43-tr:nth-child(even) td{background:#1a1b26}87+table{border-collapse:collapse;width:100%;margin:1rem 0;background:var(--bg-alt)}88+th,td{text-align:left;padding:.5rem .75rem;border:1px solid var(--border);vertical-align:top;overflow-wrap:anywhere}89+th{background:var(--bg-th);color:var(--cyan)}90+tr:nth-child(even) td{background:var(--bg)}4491 td code,th code{word-break:break-all}4592 /* List tables (m6/tables): long titles/shas wrap inside the scroll box; the # column stays narrow. */4693 td.title,td.sha{overflow-wrap:anywhere}@@ -51,20 +98,20 @@ td.num{width:1%;white-space:nowrap}5198 /* Forms: full-width on mobile, >=40px touch targets. */5299 form{max-width:44rem;margin:1rem 0;min-width:0}53100 label{display:block;margin:.75rem 0 .25rem;font-weight:600}54-input[type=text],textarea{display:block;width:100%;max-width:100%;background:#16161e;55-  color:#c0caf5;border:1px solid #292e42;padding:.5rem .6rem;font:inherit;min-height:40px}56-input[type=text]:focus,textarea:focus{outline:2px solid #7aa2f7;outline-offset:0}101+input[type=text],textarea{display:block;width:100%;max-width:100%;background:var(--bg-alt);102+  color:var(--fg);border:1px solid var(--border);padding:.5rem .6rem;font:inherit;min-height:40px}103+input[type=text]:focus,textarea:focus{outline:2px solid var(--blue);outline-offset:0}57104 textarea{min-height:12rem}58105 button,input[type=submit]{display:inline-flex;align-items:center;justify-content:center;59-  min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;border:1px solid #7aa2f7;106+  min-height:40px;min-width:40px;background:var(--blue);color:var(--btn-fg);border:1px solid var(--blue);60107   padding:.5rem 1rem;font:inherit;font-weight:700;cursor:pointer}61-button:hover,input[type=submit]:hover{background:#7dcfff}62-.muted{color:#565f89}63-.badge{display:inline-block;border:1px solid #292e42;padding:.1em .5em;font-size:.85em}64-.badge.pass{color:#9ece6a}.badge.fail{color:#f7768e}65-.badge.running{color:#e0af68}.badge.cancelled{color:#565f89}66-.notice{border:1px solid #e0af68;padding:.75rem 1rem;margin:1rem 0;overflow-wrap:anywhere}67-footer.site-footer{border-top:1px solid #292e42;color:#565f89}108+button:hover,input[type=submit]:hover{background:var(--cyan)}109+.muted{color:var(--muted)}110+.badge{display:inline-block;border:1px solid var(--border);padding:.1em .5em;font-size:.85em}111+.badge.pass{color:var(--green)}.badge.fail{color:var(--red)}112+.badge.running{color:var(--yellow)}.badge.cancelled{color:var(--muted)}113+.notice{border:1px solid var(--yellow);padding:.75rem 1rem;margin:1rem 0;overflow-wrap:anywhere}114+footer.site-footer{border-top:1px solid var(--border);color:var(--muted)}68115 footer.site-footer .wrap{padding:1rem}69116 @media (min-width:768px){70117   .cols{grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)}@@ -74,11 +121,3 @@ footer.site-footer .wrap{padding:1rem}74121 @media (min-width:1100px){75122   .wrap{max-width:72rem;padding:0 1.5rem}76123 }77-@media (prefers-color-scheme:light){78-  html,body{background:#d5d6db;color:#343b58}79-  .site-header,.site-nav,code,kbd,samp,pre,table,input[type=text],textarea{background:#cbccd3}80-  pre,pre code{color:#343b58}81-  th{background:#b4b8c7;color:#0f4b6e}82-  tr:nth-child(even) td{background:#d5d6db}83-  a{color:#34548a}84-}
tests/light_theme.rs+223 −1
diff --git a/tests/light_theme.rs b/tests/light_theme.rsnew file mode 100644index 0000000..e403ef7--- /dev/null+++ b/tests/light_theme.rs@@ -0,0 +1,223 @@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 mut out = String::with_capacity(s.len());21+    let bytes = s.as_bytes();22+    let mut i = 0;23+    while i < bytes.len() {24+        if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {25+            i += 2;26+            while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {27+                i += 1;28+            }29+            i += 2;30+        } else {31+            out.push(bytes[i] as char);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 mut out = String::with_capacity(s.len());42+    let bytes = s.as_bytes();43+    let mut i = 0;44+    while i < bytes.len() {45+        if s[i..].starts_with(":root") {46+            if let Some(open) = s[i..].find('{') {47+                let mut depth = 0;48+                let mut j = i + open;49+                while j < bytes.len() {50+                    if bytes[j] == b'{' {51+                        depth += 1;52+                    } else if bytes[j] == b'}' {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(bytes[i] as char);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:#d5d6db",130+        "--fg:#343b58",131+        "--blue:#34548a",132+        "--cyan:#0f4b6e",133+        "--red:#8c4351",134+        "--green:#33635c",135+    ] {136+        assert!(inner.contains(var), "light overrides {var}");137+    }138+}139+140+#[test]141+fn light_block_has_no_duplicated_rule_bodies() {142+    let css = css();143+    let inner = light_block_inner(&css);144+    // Light block must only redefine vars: no component selectors re-opened.145+    for sel in [146+        ".site-header",147+        ".site-nav",148+        "tr:nth-child",149+        "pre{",150+        "table{",151+    ] {152+        assert!(153+            !inner.contains(sel),154+            "light block must not duplicate rule bodies ({sel} found)"155+        );156+    }157+    assert!(inner.contains(":root"), "light overrides live in :root");158+}159+160+#[test]161+fn no_hardcoded_palette_hex_outside_root() {162+    let css = without_root_blocks(&without_comments(&css()));163+    let palette = [164+        "#1a1b26", "#16161e", "#c0caf5", "#565f89", "#7aa2f7", "#7dcfff", "#9ece6a", "#e0af68",165+        "#f7768e", "#292e42", "#1f2335", "#9aa5ce", "#d5d6db", "#343b58", "#34548a", "#0f4b6e",166+        "#8c4351", "#33635c", "#cbccd3", "#b4b8c7", "#5a4a78", "#8c6c3e", "#e9e9ed", "#233328",167+        "#38232e", "#c6d4ca", "#d9c2c7",168+    ];169+    for hex in palette {170+        assert!(171+            !css.to_lowercase().contains(hex),172+            "no hardcoded {hex} outside :root var definitions (use var(--...))"173+        );174+    }175+}176+177+#[test]178+fn rules_reference_vars() {179+    let css = css();180+    for needle in [181+        "background:var(--bg",182+        "background:var(--bg-alt",183+        "color:var(--fg",184+        "color:var(--blue",185+        "color:var(--cyan",186+        "border:1px solid var(--border",187+    ] {188+        assert!(css.contains(needle), "rules reference {needle}");189+    }190+}191+192+#[test]193+fn diff_vars_defined_dark_and_light() {194+    let css = css();195+    // Dark (current values) + light (best Tokyo Light pick; #22 may adjust).196+    for var in [197+        "--diff-add:#9ece6a",198+        "--diff-del:#f7768e",199+        "--diff-hunk:#7dcfff",200+        "--diff-add:#33635c",201+        "--diff-del:#8c4351",202+        "--diff-hunk:#0f4b6e",203+    ] {204+        assert!(css.contains(var), "diff var defined: {var}");205+    }206+    for var in ["--diff-add-bg", "--diff-del-bg", "--diff-file"] {207+        assert!(css.contains(var), "diff var present: {var}");208+    }209+    // Forward-compat classes for the #22 renderer.210+    for cls in [".diff-add", ".diff-del", ".diff-hunk", ".diff-file"] {211+        assert!(css.contains(cls), "diff class styled: {cls}");212+    }213+}214+215+#[test]216+fn light_budget_and_sharp_corners() {217+    let path = manifest().join("static").join("style.css");218+    let len = std::fs::metadata(&path).expect("style.css exists").len();219+    assert!(220+        len < 15360,221+        "static/style.css is {len} bytes, budget is 15360 (see gl0w.md)"222+    );223+}0-- 2.55.0

Comments (0)

No comments yet.

Reviews (0)

No reviews yet.

all pulls