From 3fbcb05d4dae4a03945c8b1a6a02467349fb3f1f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 2 Aug 2026 19:45:32 +0000 Subject: [PATCH] fix(web): restore CutList brand contrast --- .../Components/Layout/NavMenu.razor.css | 2 +- tests/test_nav_menu_brand_contrast.py | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/test_nav_menu_brand_contrast.py diff --git a/CutList.Web/Components/Layout/NavMenu.razor.css b/CutList.Web/Components/Layout/NavMenu.razor.css index b667c4f..4022dc2 100644 --- a/CutList.Web/Components/Layout/NavMenu.razor.css +++ b/CutList.Web/Components/Layout/NavMenu.razor.css @@ -4,7 +4,7 @@ .brand-mark i { transform: rotate(8deg); } .navbar-brand { color: #f7f3eb; font-family: Georgia, 'Times New Roman', serif; font-size: 1.45rem; font-weight: 700; letter-spacing: -.06em; line-height: .9; text-decoration: none; } .navbar-brand .brand-cut { color: #f26b38; } -.navbar-brand .brand-list { color: #fffaf0; } +.navbar-brand .brand-list { color: #20231f; } .navbar-brand small { color: #8f968d; display: block; font-family: Arial, sans-serif; font-size: .48rem; font-weight: 700; letter-spacing: .14em; margin-top: .45rem; } .nav-section-label { color: #737a73; font-size: .62rem; font-weight: 700; letter-spacing: .13em; margin: 1.35rem 1.5rem .5rem; text-transform: uppercase; } .nav-item { padding: .1rem .75rem; } diff --git a/tests/test_nav_menu_brand_contrast.py b/tests/test_nav_menu_brand_contrast.py new file mode 100644 index 0000000..d183a2e --- /dev/null +++ b/tests/test_nav_menu_brand_contrast.py @@ -0,0 +1,49 @@ +"""Regression checks for the CutList navigation brand on its light header.""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +NAV_MENU = REPO_ROOT / "CutList.Web/Components/Layout/NavMenu.razor" +NAV_MENU_CSS = REPO_ROOT / "CutList.Web/Components/Layout/NavMenu.razor.css" +APP_CSS = REPO_ROOT / "CutList.Web/wwwroot/css/app.css" + + +def selector_color(css: str, selector: str) -> str: + match = re.search(rf"{re.escape(selector)}\s*\{{([^}}]*)\}}", css) + if match is None: + raise AssertionError(f"Missing CSS selector: {selector}") + + color = re.search(r"\bcolor:\s*(#[0-9a-fA-F]{6})", match.group(1)) + if color is None: + raise AssertionError(f"Missing hex color for selector: {selector}") + return color.group(1) + + +def contrast_ratio(foreground: str, background: str) -> float: + def luminance(color: str) -> float: + channels = [int(color[index:index + 2], 16) / 255 for index in (1, 3, 5)] + linear = [value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4 for value in channels] + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] + + lighter, darker = sorted((luminance(foreground), luminance(background)), reverse=True) + return (lighter + 0.05) / (darker + 0.05) + + +class NavMenuBrandContrastTests(unittest.TestCase): + def test_list_word_has_accessible_contrast_against_light_top_row(self) -> None: + list_color = selector_color(NAV_MENU_CSS.read_text(), ".navbar-brand .brand-list") + top_row_color = selector_color(APP_CSS.read_text(), ".top-row") + + self.assertGreaterEqual(contrast_ratio(list_color, top_row_color), 4.5) + + def test_brand_keeps_cut_and_list_as_adjacent_words(self) -> None: + markup = NAV_MENU.read_text() + self.assertIn('CutList', markup) + + +if __name__ == "__main__": + unittest.main()