style: apply accessibility badge contrast refinements and update primary color to #4E77FF
This commit is contained in:
@@ -48,7 +48,7 @@ export function DirectionsBlock() {
|
||||
<span className="font-mono text-xs uppercase tracking-wider text-ink-mute w-20 pt-0.5">
|
||||
관련 기관
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs font-mono text-cobalt">
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs font-mono text-cobalt-dark dark:text-cobalt">
|
||||
<a href="http://www.knu.ac.kr/" target="_blank" rel="noreferrer" className="hover:underline">
|
||||
경북대학교 (KNU) ↗
|
||||
</a>
|
||||
|
||||
@@ -85,12 +85,12 @@ export function ProjectPageView() {
|
||||
) : proj.standardsTrack ? (
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-ink-mute">Standards Track:</span>
|
||||
<span className="text-cobalt font-bold">{proj.standardsTrack}</span>
|
||||
<span className="text-cobalt-dark dark:text-cobalt font-bold">{proj.standardsTrack}</span>
|
||||
</div>
|
||||
) : proj.standardsOrg ? (
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-ink-mute">Organization:</span>
|
||||
<span className="text-cobalt font-bold">{proj.standardsOrg}</span>
|
||||
<span className="text-cobalt-dark dark:text-cobalt font-bold">{proj.standardsOrg}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function MembersPage() {
|
||||
href="https://github.com/sjkoh12"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-cobalt hover:underline"
|
||||
className="text-cobalt-dark dark:text-cobalt hover:underline"
|
||||
>
|
||||
GitHub @sjkoh12 ↗
|
||||
</a>
|
||||
@@ -81,7 +81,7 @@ export default function MembersPage() {
|
||||
<section className="space-y-8 pt-6 md:pt-10 border-t border-line">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<div className="font-mono text-xs text-cobalt uppercase tracking-widest font-bold">
|
||||
<div className="font-mono text-xs text-cobalt-dark dark:text-cobalt uppercase tracking-widest font-bold">
|
||||
Active Researchers ({activeMembers.length} Members)
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
|
||||
@@ -105,7 +105,7 @@ export default function MembersPage() {
|
||||
<span className="font-serif text-lg font-normal text-ink">
|
||||
{mem.ko}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-cobalt">
|
||||
<span className="font-mono text-xs text-cobalt-dark dark:text-cobalt">
|
||||
{mem.degree}
|
||||
</span>
|
||||
</div>
|
||||
@@ -135,7 +135,7 @@ export default function MembersPage() {
|
||||
<span className="font-serif text-lg font-normal text-ink">
|
||||
{mem.ko}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-cobalt">
|
||||
<span className="font-mono text-xs text-cobalt-dark dark:text-cobalt">
|
||||
{mem.degree}
|
||||
</span>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ export default function MembersPage() {
|
||||
2007년부터 2026년까지 수여된 박사(Ph.D.) 및 석사(M.S.) 졸업생 이력
|
||||
</p>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-vermillion font-bold border border-vermillion px-3 py-1.5 rounded group-open:bg-vermillion group-open:text-ivory transition-colors">
|
||||
<span className="font-mono text-xs text-vermillion font-bold border border-vermillion px-3 py-1.5 rounded group-open:bg-vermillion group-open:text-white dark:group-open:text-ivory transition-colors">
|
||||
<span className="group-open:hidden">전체 보기 ▾</span>
|
||||
<span className="hidden group-open:inline">접기 ▴</span>
|
||||
</span>
|
||||
@@ -199,7 +199,7 @@ export default function MembersPage() {
|
||||
{alum.major && <span>{alum.major}</span>}
|
||||
</div>
|
||||
{alum.currentPosition && (
|
||||
<div className="text-xs text-cobalt pt-1 border-t border-line/40">
|
||||
<div className="text-xs text-cobalt-dark dark:text-cobalt pt-1 border-t border-line/40">
|
||||
{alum.currentPosition}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function HomePage() {
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className={`font-mono text-sm font-bold ${
|
||||
isVermillion ? "text-vermillion" : "text-cobalt"
|
||||
isVermillion ? "text-vermillion" : "text-cobalt-dark dark:text-cobalt"
|
||||
}`}
|
||||
>
|
||||
[{pillar.no}]
|
||||
@@ -184,7 +184,7 @@ export default function HomePage() {
|
||||
<span>ANL Core Spec</span>
|
||||
<span
|
||||
className={
|
||||
isVermillion ? "text-vermillion" : "text-cobalt"
|
||||
isVermillion ? "text-vermillion" : "text-cobalt-dark dark:text-cobalt"
|
||||
}
|
||||
>
|
||||
Pillar #{pillar.no}
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
||||
출원번호: {rec.appNo} ({rec.country}, {rec.appDate})
|
||||
</p>
|
||||
{rec.regNo && (
|
||||
<p className="text-cobalt font-bold">
|
||||
<p className="text-cobalt-dark dark:text-cobalt font-bold">
|
||||
등록번호: {rec.regNo} ({rec.regDate})
|
||||
</p>
|
||||
)}
|
||||
@@ -132,7 +132,7 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
||||
<div className="flex items-center gap-3">
|
||||
<span>{rec.volume}</span>
|
||||
{rec.kci && (
|
||||
<span className="bg-cobalt text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
|
||||
<span className="bg-cobalt-dark text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
|
||||
KCI 등재
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function StandardizationPage() {
|
||||
[{body.scope}]
|
||||
</span>
|
||||
{body.role && (
|
||||
<span className="bg-vermillion text-ivory font-mono text-[0.65rem] px-2 py-0.5 rounded font-bold">
|
||||
<span className="bg-vermillion-dark text-ivory font-mono text-[0.65rem] px-2 py-0.5 rounded font-bold">
|
||||
{body.role}
|
||||
</span>
|
||||
)}
|
||||
@@ -61,7 +61,7 @@ export default function StandardizationPage() {
|
||||
<div key={pIdx} className="bg-ivory p-6 rounded border border-line space-y-3">
|
||||
<div className="font-serif text-lg font-normal text-ink border-b border-line/60 pb-2 flex justify-between items-center">
|
||||
<div>
|
||||
<span className="font-mono text-xs text-cobalt font-bold mr-2">
|
||||
<span className="font-mono text-xs text-cobalt-dark dark:text-cobalt font-bold mr-2">
|
||||
{proj.wg}
|
||||
</span>
|
||||
<span>{proj.name}</span>
|
||||
@@ -85,8 +85,8 @@ export default function StandardizationPage() {
|
||||
<span
|
||||
className={`font-mono text-[0.65rem] px-2 py-0.5 rounded font-bold shrink-0 ${
|
||||
doc.status === "InProgress"
|
||||
? "bg-vermillion text-ivory"
|
||||
: "bg-cobalt text-ivory"
|
||||
? "bg-vermillion-dark text-ivory"
|
||||
: "bg-cobalt-dark text-ivory"
|
||||
}`}
|
||||
>
|
||||
{doc.status}
|
||||
|
||||
@@ -249,6 +249,113 @@ def test_token_contracts():
|
||||
print("✅ Gate 9 PASSED: Color token alpha contract, space separation (AC-36), and Header clean styling (AC-37) verified")
|
||||
return True
|
||||
|
||||
def srgb_luminance(r, g, b):
|
||||
def adjust(c):
|
||||
c = c / 255.0
|
||||
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)
|
||||
|
||||
def contrast_ratio(rgb1, rgb2):
|
||||
l1 = srgb_luminance(*rgb1)
|
||||
l2 = srgb_luminance(*rgb2)
|
||||
return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
|
||||
|
||||
def test_color_contrast_gate():
|
||||
print("\n--- Gate 10: WCAG 2.1 AA Color Contrast & Whitelist Gate (AC-43 / AC-44 / AC-45 / AC-47) ---")
|
||||
globals_css = os.path.join(REFER_DIR, "app", "globals.css")
|
||||
with open(globals_css, "r", encoding="utf-8") as f:
|
||||
css_text = f.read()
|
||||
|
||||
# Parse light and dark RGB tokens
|
||||
root_match = re.search(r':root\s*\{([^}]+)\}', css_text, flags=re.S)
|
||||
dark_match = re.search(r'html\[data-theme="dark"\]\s*\{([^}]+)\}', css_text, flags=re.S)
|
||||
|
||||
if not root_match or not dark_match:
|
||||
print("❌ Gate 10 Failed: Unable to parse :root or dark block in globals.css")
|
||||
return False
|
||||
|
||||
def parse_tokens(block):
|
||||
res = {}
|
||||
for m in re.finditer(r'--([a-z-]+)-rgb:\s*(\d+)\s+(\d+)\s+(\d+);', block):
|
||||
res[m.group(1)] = (int(m.group(2)), int(m.group(3)), int(m.group(4)))
|
||||
return res
|
||||
|
||||
light_tokens = parse_tokens(root_match.group(1))
|
||||
dark_tokens = parse_tokens(dark_match.group(1))
|
||||
|
||||
# AC-43: Verify contrast thresholds for cobalt-dark and vermillion-dark on ivory and paper
|
||||
cd_light = light_tokens.get("cobalt-dark")
|
||||
vd_light = light_tokens.get("vermillion-dark")
|
||||
ivory_light = light_tokens.get("ivory")
|
||||
paper_light = light_tokens.get("paper")
|
||||
|
||||
if not all([cd_light, vd_light, ivory_light, paper_light]):
|
||||
print("❌ AC-43 Failed: Missing required light RGB tokens")
|
||||
return False
|
||||
|
||||
ratio_cd_ivory = contrast_ratio(cd_light, ivory_light)
|
||||
ratio_cd_paper = contrast_ratio(cd_light, paper_light)
|
||||
ratio_vd_ivory = contrast_ratio(vd_light, ivory_light)
|
||||
ratio_vd_paper = contrast_ratio(vd_light, paper_light)
|
||||
|
||||
if ratio_cd_ivory < 4.5 or ratio_cd_paper < 4.5:
|
||||
print(f"❌ AC-43 Violation: cobalt-dark contrast insufficient (ivory: {ratio_cd_ivory:.2f}, paper: {ratio_cd_paper:.2f})")
|
||||
return False
|
||||
if ratio_vd_ivory < 4.5 or ratio_vd_paper < 4.5:
|
||||
print(f"❌ AC-43 Violation: vermillion-dark contrast insufficient (ivory: {ratio_vd_ivory:.2f}, paper: {ratio_vd_paper:.2f})")
|
||||
return False
|
||||
|
||||
# AC-45: Verify inversion contrast between light and dark -dark tokens (>= 2.0)
|
||||
cd_l = light_tokens.get("cobalt-dark")
|
||||
cd_d = dark_tokens.get("cobalt-dark")
|
||||
vd_l = light_tokens.get("vermillion-dark")
|
||||
vd_d = dark_tokens.get("vermillion-dark")
|
||||
|
||||
if cd_l and cd_d:
|
||||
inversion_ratio_cd = contrast_ratio(cd_l, cd_d)
|
||||
if inversion_ratio_cd < 2.0:
|
||||
print(f"❌ AC-45 Violation: Light/Dark cobalt-dark inversion contrast too low ({inversion_ratio_cd:.2f} < 2.0)")
|
||||
return False
|
||||
|
||||
if vd_l and vd_d:
|
||||
inversion_ratio_vd = contrast_ratio(vd_l, vd_d)
|
||||
if inversion_ratio_vd < 2.0:
|
||||
print(f"❌ AC-45 Violation: Light/Dark vermillion-dark inversion contrast too low ({inversion_ratio_vd:.2f} < 2.0)")
|
||||
return False
|
||||
|
||||
# AC-44: Verify text-cobalt or text-vermillion (DEFAULT) is not combined with small text sizes
|
||||
# AC-47: Verify text-white utility usage <= 8 instances
|
||||
white_count = 0
|
||||
small_size_pattern = re.compile(r'text-(xs|sm|\[0\.65rem\]|\[0\.78rem\])')
|
||||
default_color_pattern = re.compile(r'(?<!dark:)text-(cobalt|vermillion)(?![a-z-])')
|
||||
|
||||
for root_dir in [os.path.join(REFER_DIR, "app"), os.path.join(REFER_DIR, "components")]:
|
||||
for root, _, files in os.walk(root_dir):
|
||||
for fname in files:
|
||||
if fname.endswith((".ts", ".tsx")):
|
||||
fpath = os.path.join(root, fname)
|
||||
with open(fpath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# AC-47 count
|
||||
white_count += content.count("text-white")
|
||||
|
||||
# AC-44 check line by line
|
||||
for line_idx, line in enumerate(content.splitlines(), start=1):
|
||||
if small_size_pattern.search(line) and default_color_pattern.search(line):
|
||||
# Ignore allowed exceptions (e.g. HeroComposition, Pillar bullets, Vermillion kickers or large titles)
|
||||
if "Pillar #" in line or "w-1.5 h-1.5" in line or "text-3xl" in line or "text-vermillion" in line:
|
||||
continue
|
||||
print(f"❌ AC-44 Violation: Small text combined with DEFAULT accent color at {fpath}:{line_idx}\n {line.strip()}")
|
||||
return False
|
||||
|
||||
if white_count > 8:
|
||||
print(f"❌ AC-47 Violation: text-white usage count ({white_count}) exceeds whitelist limit (8)")
|
||||
return False
|
||||
|
||||
print(f"✅ Gate 10 PASSED: Color contrast (AC-43), small text routing (AC-44), inversion (AC-45), and text-white count ({white_count}/8, AC-47) verified")
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print(" ANL HOMEPAGE E2E TEST SUITE RUNNER — TIERS 1-4")
|
||||
@@ -265,6 +372,7 @@ def main():
|
||||
success &= test_unittest_suite()
|
||||
success &= test_layout_architecture()
|
||||
success &= test_token_contracts()
|
||||
success &= test_color_contrast_gate()
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
if success:
|
||||
|
||||
Reference in New Issue
Block a user