From 70bad93cdeb4fe114e3202fff7b3b091c8bcbc84 Mon Sep 17 00:00:00 2001 From: Eric Boehs Date: Sat, 29 Aug 2026 22:16:08 -0500 Subject: [PATCH 1/2] feat: pure black dark mode with blue story links Dark mode rendered every link white, because UIkit's .uk-light sets `a { color: #fff }`. That erased the distinction between a story title and ordinary text. It also sets `.uk-text-primary { color: #fff !important }`, which silently removed the blue high-score highlight, so a 328-point story looked identical to a 55-point one. Story links and the score highlight are now #58a6ff. Measured against the new background it is 8.3:1, clearing WCAG AAA; UIkit's own #1e87f0 only reaches 5.8:1 and fails AA outright on the previous #222. Background is pure black so OLED pixels switch off rather than draw dark grey. That made the #DDD thread borders glaring -- 15:1 against the page, louder than the text beside them -- so they drop to #2a2a2a, with the hover border reusing the link blue. The dark palette is now gated behind .hn-dark, which the script adds. Previously the dark background came from the media query alone while the text colour came from .uk-light via JS, so with JS disabled the page rendered #666 text on a #222 background: a contrast ratio of 1.9:1. It now falls back to the light palette, which is at least readable. The script moves to and runs synchronously so the background is set before first paint instead of flashing white, and it listens for media-query changes rather than re-checking twice a second for the life of the tab. Verified with Chrome over the DevTools protocol, emulating each colour scheme and reading computed styles: dark gives rgb(0,0,0) background with rgb(88,166,255) links and score, borders rgb(42,42,42); with script execution disabled the html element carries no class and the background stays white. --- tests/app_test.rb | 14 +++++++++++ views/layout.erb | 62 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/tests/app_test.rb b/tests/app_test.rb index f98b56c..789f995 100644 --- a/tests/app_test.rb +++ b/tests/app_test.rb @@ -318,6 +318,20 @@ def test_ordinary_urls_still_render # --- caching -------------------------------------------------------------- + # The dark palette is gated on .hn-dark, which only JS adds. If that gate is + # ever removed, dark mode renders #666 text on a black background. + def test_dark_mode_styles_are_scoped_so_the_page_survives_without_js + create_item id: 1, title: 'A', score: 100 + + get '/' + body = last_response.body + + assert_includes body, 'prefers-color-scheme: dark' + assert_includes body, 'html.hn-dark, html.hn-dark body { background-color: #000 }' + # Driven by a media-query listener rather than a timer. + assert_includes body, "addEventListener('change', applyScheme)" + end + def test_homepage_sends_cache_validators create_item id: 1, title: 'A', score: 100 diff --git a/views/layout.erb b/views/layout.erb index 57f5d6e..9cdbb0c 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -40,10 +40,55 @@ a { color: #0d0d0d; } } + /* Dark mode is applied by the script in , which adds .hn-dark to + and UIkit's .uk-light to . Scoping the dark palette to + .hn-dark means that with JS disabled the page stays on the readable + light palette rather than dark text on a dark background. */ @media (prefers-color-scheme: dark) { - html { background-color: rgb(34, 34, 34) } + /* Pure black rather than #222 so OLED pixels switch off entirely. */ + html.hn-dark, html.hn-dark body { background-color: #000 } + + /* .uk-light paints every link #fff, which erases the difference + between a story title and ordinary text. #58a6ff measures 8.3:1 on + black (WCAG AAA); UIkit's own #1e87f0 only reaches 5.8:1. */ + .hn-dark .uk-light .story a, + .hn-dark .uk-light table a { color: #58a6ff } + + /* Visited stories stay dimmed, the way HN does it. This has to follow + the rule above: the two selectors have equal specificity. */ + .hn-dark .uk-light table a:visited { color: #999 } + + /* .uk-light also forces .uk-text-primary to #fff !important, which + loses the high-score highlight completely. */ + .hn-dark .uk-light .uk-text-primary { color: #58a6ff !important } + + /* #DDD thread borders measure 15:1 on black, louder than the text + they sit beside. */ + .hn-dark .uk-comment-list li { border-left-color: #2a2a2a } + .hn-dark .uk-comment-list li:hover { border-left-color: #58a6ff } } + +
@@ -53,21 +98,6 @@ <%= yield %> - - - <% if ENV['GA_TRACKING_ID'] %>