diff --git a/resources/tester/test_200_1_b09_box_tool_line.py b/resources/tester/test_200_1_b09_box_tool_line.py index 57d9e08fc..eea635ee6 100644 --- a/resources/tester/test_200_1_b09_box_tool_line.py +++ b/resources/tester/test_200_1_b09_box_tool_line.py @@ -116,3 +116,85 @@ def test_잇기(): assert "attachHoverInfo(hint," in groups cards = (ROOT / COMMON).read_text("utf-8") assert "if (opts.lead) bar.append(opts.lead);" in cards + + +# ── 200-3 카드 제목 줄 · 합계 줄 바탕색(밝음 · 어두움) ───────────────────────── + + +def _theme_vars(dark: bool) -> dict: + """테마 CSS 의 `:root`(밝음) · 어두움 덮어쓰기 토큰 → 한 벌.""" + css = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") + light = dict(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ":root"))) + if dark: + light.update(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ':root[data-theme="dark"]'))) + return light + + +def _rgb(token: str, theme: dict) -> tuple: + """토큰 → (r, g, b) · var() 별칭 따라감 · 16진수만.""" + val = theme[token].split("/*")[0].strip() + while m := re.fullmatch(r"var\((--[\w-]+)\)", val): + val = theme[m.group(1)].split("/*")[0].strip() + assert re.fullmatch(r"#[0-9a-fA-F]{6}", val), (token, val) + return tuple(int(val[i : i + 2], 16) for i in (1, 3, 5)) + + +def _mix(spec: str, theme: dict) -> tuple: + """`color-mix(in srgb, var(A) N%, var(B))` → 섞은 (r, g, b).""" + m = re.fullmatch( + r"color-mix\(in srgb,\s*var\((--[\w-]+)\)\s*(\d+)%,\s*var\((--[\w-]+)\)\)", spec + ) + assert m, spec + a, n, b = _rgb(m.group(1), theme), int(m.group(2)) / 100, _rgb(m.group(3), theme) + return tuple(a[i] * n + b[i] * (1 - n) for i in range(3)) + + +def _lum(c: tuple) -> float: + ch = [ + (v / 255) / 12.92 if v / 255 <= 0.03928 else (((v / 255) + 0.055) / 1.055) ** 2.4 for v in c + ] + return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2] + + +def _contrast(a: tuple, b: tuple) -> float: + hi, lo = sorted((_lum(a), _lum(b)), reverse=True) + return (hi + 0.05) / (lo + 0.05) + + +def _tokens() -> tuple: + css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") + root = _block(css, ".ui-group-cards") + head = re.search(r"--group-head-bg:\s*([^;]+);", root) + foot = re.search(r"--group-foot-bg:\s*([^;]+);", root) + assert head and foot + return css, head.group(1), foot.group(1) + + +def test_카드_제목_줄과_합계_줄_바탕색_토큰(): + css, head, foot = _tokens() + assert head != foot + assert "background-color: var(--group-head-bg)" in _block(css, ".ui-group-card__head") + assert "background-color: var(--group-foot-bg)" in _block(css, ".ui-group-card__foot") + for spec in (head, foot): # 고른 값 없이 테마 변수만(색 고정값 없음) + assert "#" not in spec and "rgb(" not in spec + + +def test_밝음_어두움_두_색이_다르고_글_대비_유지(): + _, head, foot = _tokens() + for dark in (False, True): + theme = _theme_vars(dark) + h, f = _mix(head, theme), _mix(foot, theme) + assert sum(abs(h[i] - f[i]) for i in range(3)) >= 40, (dark, h, f) # 서로 다른 색 + card = _rgb("--color-surface-raised", theme) + assert sum(abs(f[i] - card[i]) for i in range(3)) >= 40 # 카드 바탕과도 구분 + for color in ("--color-text", "--color-text-secondary", "--color-accent"): + assert _contrast(_rgb(color, theme), h) >= 4.5, (dark, "head", color) + for color in ("--color-text", "--color-text-body", "--color-text-secondary"): + assert _contrast(_rgb(color, theme), f) >= 4.5, (dark, "foot", color) + + +def test_어두움_테마_값(): + # 어두움 테마에서 쓰는 토큰이 실제로 어두움 값으로 덮임(밝음 값 그대로가 아님) + light, dark = _theme_vars(False), _theme_vars(True) + for token in ("--color-surface", "--color-surface-raised", "--color-accent"): + assert _rgb(token, light) != _rgb(token, dark), token diff --git a/ui_template/ui_template_group_cards.css b/ui_template/ui_template_group_cards.css index ed4f4f584..677c99a50 100644 --- a/ui_template/ui_template_group_cards.css +++ b/ui_template/ui_template_group_cards.css @@ -1,6 +1,9 @@ /* 묶음 카드 공용 양식(171-2) — 글자 14px 한 벌 · 줄 간격 좁게 · 숫자 오른쪽. 색은 테마 변수만. */ .ui-group-cards { + /* 200-3 제목 줄 = 표 머리색 계열(옅게) · 합계 줄 = 강조색 더 짙게 — 둘 다 테마 변수에서 섞음(밝음 · 어두움 같이) */ + --group-head-bg: color-mix(in srgb, var(--color-accent) 6%, var(--color-surface-raised)); + --group-foot-bg: color-mix(in srgb, var(--color-accent) 15%, var(--color-surface-raised)); display: flex; flex-direction: column; gap: var(--spacing-8); @@ -68,7 +71,8 @@ flex-wrap: wrap; align-items: baseline; column-gap: var(--spacing-8); - padding: 2px 0; + padding: 2px var(--spacing-4); + background-color: var(--group-head-bg); border-bottom: 1px solid var(--color-border); font-weight: var(--font-weight-bold); } @@ -148,6 +152,7 @@ justify-content: flex-end; column-gap: var(--spacing-8); padding: 2px var(--spacing-4); + background-color: var(--group-foot-bg); border-top: 1px solid var(--color-border); font-variant-numeric: tabular-nums; }