feat(B09): 박스보기 카드 제목 줄 · 합계 줄 바탕색 구분(밝음 · 어두움 테마 토큰)(200-3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
This commit is contained in:
2026-10-10 14:15:05 +09:00
co-authored by Claude Sonnet 5.5
parent 8f169f5fe2
commit 84ae5100f8
2 changed files with 88 additions and 1 deletions
@@ -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
+6 -1
View File
@@ -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;
}