#17 m6/light-theme
Patch
static/style.css+76 −37--- 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--- /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--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.