a11y: accessibility improvements (WCAG 2.1 AA)

- Add skip link for keyboard navigation
- Add ARIA labels to interactive elements
- Add aria-hidden to decorative elements (ASCII logo, icons)
- Add aria-live regions for dynamic content
- Add scope="col" to table headers
- Fix footer contrast (remove opacity-50)
- Add prefers-reduced-motion support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-30 16:52:02 +09:00
parent 40d56d05e8
commit b9094a4013

View File

@@ -82,6 +82,29 @@
/* Font families */
.font-display { font-family: "Space Grotesk", sans-serif; }
.font-mono { font-family: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
/* Accessibility - Screen Reader Only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.focus\:not-sr-only:focus {
position: absolute;
width: auto;
height: auto;
padding: 0;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
</style>
<style>
::-webkit-scrollbar { width: 10px; height: 10px; }
@@ -100,28 +123,42 @@
.typing-effect { overflow: hidden; white-space: nowrap; animation: typing 2s steps(40, end); }
@keyframes typing { from { width: 0 } to { width: 100% } }
/* Respect user's motion preferences */
@media (prefers-reduced-motion: reduce) {
.cursor-blink { animation: none; }
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body class="bg-background-dark text-terminal-text font-mono min-h-screen flex flex-col overflow-x-hidden selection:bg-terminal-cyan selection:text-background-dark">
<!-- Skip Link for Accessibility -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-background-dark focus:rounded focus:outline-none">
메인 콘텐츠로 건너뛰기
</a>
<div class="layout-container flex h-full grow flex-col items-center justify-center p-4 md:p-8 lg:p-12">
<!-- Main Terminal Window -->
<div class="w-full max-w-[1400px] bg-background-dark border border-terminal-border rounded-lg shadow-[0_0_50px_-12px_rgba(0,0,0,0.7)] overflow-hidden flex flex-col">
<!-- Window Title Bar -->
<div class="bg-[#161b22] px-4 py-3 flex items-center justify-between border-b border-terminal-border select-none">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2" aria-hidden="true">
<div class="w-3 h-3 rounded-full bg-[#ff5f56]"></div>
<div class="w-3 h-3 rounded-full bg-[#ffbd2e]"></div>
<div class="w-3 h-3 rounded-full bg-[#27c93f]"></div>
</div>
<div class="flex items-center gap-2 opacity-70">
<span class="material-symbols-outlined text-[14px]">terminal</span>
<span class="material-symbols-outlined text-[14px]" aria-hidden="true">terminal</span>
<span class="text-xs md:text-sm text-terminal-muted font-display">root@anvil-cloud:~</span>
</div>
<a href="https://t.me/AnvilForgeBot" target="_blank" class="text-xs text-terminal-muted hover:text-primary transition flex items-center gap-1">
<a href="https://t.me/AnvilForgeBot" target="_blank" class="text-xs text-terminal-muted hover:text-primary transition flex items-center gap-1" aria-label="텔레그램 봇 @AnvilForgeBot 열기">
<span>@AnvilForgeBot</span>
<span class="material-symbols-outlined text-sm">open_in_new</span>
<span class="material-symbols-outlined text-sm" aria-hidden="true">open_in_new</span>
</a>
</div>
@@ -140,10 +177,10 @@
</div>
<!-- Hero Section -->
<div class="flex flex-col gap-6 z-10">
<div class="flex flex-col gap-6 z-10" id="main-content">
<!-- ASCII Art Logo -->
<div class="flex items-end gap-3">
<pre class="text-primary font-bold leading-[0.85] text-[8px] sm:text-[10px] md:text-xs select-none"> █████╗ ███╗ ██╗██╗ ██╗██╗██╗
<pre aria-hidden="true" class="text-primary font-bold leading-[0.85] text-[8px] sm:text-[10px] md:text-xs select-none"> █████╗ ███╗ ██╗██╗ ██╗██╗██╗
██╔══██╗████╗ ██║██║ ██║██║██║
███████║██╔██╗ ██║██║ ██║██║██║
██╔══██║██║╚██╗██║╚██╗ ██╔╝██║██║
@@ -154,7 +191,7 @@
<!-- Main Headline with Rotation -->
<div class="space-y-6 max-w-4xl mt-4" x-data="headlineRotator()">
<div class="min-h-[120px] md:min-h-[160px] lg:min-h-[200px] flex flex-col justify-center">
<div class="min-h-[120px] md:min-h-[160px] lg:min-h-[200px] flex flex-col justify-center" aria-live="polite" aria-atomic="true">
<h1 class="text-2xl md:text-4xl lg:text-5xl font-display font-bold text-white tracking-tight leading-tight">
<span class="text-terminal-muted mr-2">&gt;</span><span x-text="displayedLine1"></span><span x-show="isTypingLine1" class="text-primary cursor-blink">_</span><br/>
<span class="text-terminal-muted mr-2">&gt;</span><span class="text-primary" x-text="displayedLine2"></span><span x-show="isTypingLine2 || (!isTypingLine1 && !isTypingLine2)" class="text-primary cursor-blink">_</span>
@@ -181,7 +218,7 @@
</div>
<!-- Result Message -->
<div class="mt-4 text-terminal-muted text-sm border-l-2 border-primary/50 pl-3 transition-opacity duration-300"
:class="showResult ? 'opacity-100' : 'opacity-0'">
:class="showResult ? 'opacity-100' : 'opacity-0'" aria-live="polite">
<span class="text-primary"></span>
<span x-text="commands[currentIndex]?.result"></span>
</div>
@@ -260,23 +297,23 @@
<table class="w-full text-sm">
<thead>
<tr class="border-b border-terminal-border bg-[#161b22] text-left">
<th class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white" @click="toggleSort('name')">
<th scope="col" class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white" @click="toggleSort('name')">
NAME <span x-show="sortBy === 'name'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white text-center" @click="toggleSort('vcpu')">
<th scope="col" class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white text-center" @click="toggleSort('vcpu')">
CPU <span x-show="sortBy === 'vcpu'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white text-center" @click="toggleSort('memory')">
<th scope="col" class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white text-center" @click="toggleSort('memory')">
RAM <span x-show="sortBy === 'memory'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th class="px-4 py-3 font-bold text-terminal-muted text-center hidden sm:table-cell">DISK</th>
<th class="px-4 py-3 font-bold text-terminal-muted text-center hidden md:table-cell">TRAFFIC</th>
<th class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white text-right" @click="toggleSort('price')">
<th scope="col" class="px-4 py-3 font-bold text-terminal-muted text-center hidden sm:table-cell">DISK</th>
<th scope="col" class="px-4 py-3 font-bold text-terminal-muted text-center hidden md:table-cell">TRAFFIC</th>
<th scope="col" class="px-4 py-3 font-bold text-terminal-muted cursor-pointer hover:text-white text-right" @click="toggleSort('price')">
PRICE/MO <span x-show="sortBy === 'price'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
</tr>
</thead>
<tbody>
<tbody aria-live="polite">
<!-- Loading State -->
<template x-if="loading">
<tr>
@@ -325,8 +362,8 @@
<span class="text-primary" x-text="filteredInstances.length"></span> instances
<span x-show="fromCache" class="text-terminal-amber ml-2">(cached)</span>
</span>
<button @click="forceRefresh()" class="hover:text-primary transition-colors flex items-center gap-1" :disabled="loading">
<span class="material-symbols-outlined text-sm" :class="loading && 'animate-spin'">refresh</span>
<button @click="forceRefresh()" class="hover:text-primary transition-colors flex items-center gap-1" :disabled="loading" aria-label="가격 정보 새로고침">
<span class="material-symbols-outlined text-sm" :class="loading && 'animate-spin'" aria-hidden="true">refresh</span>
refresh
</button>
</div>
@@ -392,7 +429,7 @@
</div>
</div>
<footer class="mt-8 text-terminal-muted text-xs text-center opacity-50">
<footer class="mt-8 text-terminal-muted text-xs text-center">
© 2024 Anvil Hosting. All containers running. PID 1 healthy.
</footer>
</div>