e757d4ee30
A break timer designed to prevent RSI should be usable by people who already live with disabilities. This overhaul adds comprehensive accessibility without changing the visual design. Changes across 17 source files: - Global focus-visible outlines, sr-only utility, forced-colors support - prefers-reduced-motion kills all CSS animations AND JS Web Animations - All text upgraded to 4.5:1+ contrast ratio (WCAG AA) - Keyboard navigation for ColorPicker, Stepper, TimeSpinner - Screen reader: aria-live status regions, progressbar roles, labeled controls, sr-only chart data table, focus management on view changes - Focus trap on break screen, aria-hidden on decorative elements - Descriptive labels on all 25+ toggle/stepper instances in Settings - README updated with accessibility section and WCAG badge
122 lines
3.5 KiB
Svelte
122 lines
3.5 KiB
Svelte
<script lang="ts">
|
|
interface Props {
|
|
accentColor: string;
|
|
breakColor: string;
|
|
}
|
|
|
|
let { accentColor, breakColor }: Props = $props();
|
|
|
|
let reducedMotion = $state(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
$effect(() => {
|
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
const handler = (e: MediaQueryListEvent) => { reducedMotion = e.matches; };
|
|
mq.addEventListener("change", handler);
|
|
return () => mq.removeEventListener("change", handler);
|
|
});
|
|
</script>
|
|
|
|
<div class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
|
|
<!-- Gradient blobs -->
|
|
<div
|
|
class="blob blob-1"
|
|
style="background: radial-gradient(circle, {accentColor} 0%, transparent 70%);"
|
|
></div>
|
|
<div
|
|
class="blob blob-2"
|
|
style="background: radial-gradient(circle, {breakColor} 0%, transparent 70%);"
|
|
></div>
|
|
<div
|
|
class="blob blob-3"
|
|
style="background: radial-gradient(circle, {accentColor} 0%, transparent 70%);"
|
|
></div>
|
|
<div
|
|
class="blob blob-4"
|
|
style="background: radial-gradient(circle, {breakColor} 0%, transparent 70%);"
|
|
></div>
|
|
|
|
<!-- Film grain overlay -->
|
|
<svg class="absolute inset-0 h-full w-full" style="opacity: 0.08; mix-blend-mode: overlay;">
|
|
<filter id="grain-filter">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.45" numOctaves="3" stitchTiles="stitch">
|
|
{#if !reducedMotion}
|
|
<animate
|
|
attributeName="seed"
|
|
from="0"
|
|
to="100"
|
|
dur="2s"
|
|
repeatCount="indefinite"
|
|
/>
|
|
{/if}
|
|
</feTurbulence>
|
|
</filter>
|
|
<rect width="100%" height="100%" filter="url(#grain-filter)" />
|
|
</svg>
|
|
</div>
|
|
|
|
<style>
|
|
.blob {
|
|
position: absolute;
|
|
width: 300px;
|
|
height: 300px;
|
|
border-radius: 50%;
|
|
filter: blur(80px);
|
|
opacity: 0.4;
|
|
will-change: transform;
|
|
}
|
|
|
|
.blob-1 {
|
|
top: -15%;
|
|
left: -10%;
|
|
animation: drift-1 30s ease-in-out infinite;
|
|
}
|
|
.blob-2 {
|
|
bottom: -15%;
|
|
right: -10%;
|
|
animation: drift-2 35s ease-in-out infinite;
|
|
}
|
|
.blob-3 {
|
|
top: 40%;
|
|
right: -15%;
|
|
width: 250px;
|
|
height: 250px;
|
|
animation: drift-3 28s ease-in-out infinite;
|
|
}
|
|
.blob-4 {
|
|
bottom: 20%;
|
|
left: -15%;
|
|
width: 200px;
|
|
height: 200px;
|
|
animation: drift-4 32s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes drift-1 {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
20% { transform: translate(300px, 200px) scale(1.15); }
|
|
40% { transform: translate(150px, 450px) scale(0.9); }
|
|
60% { transform: translate(350px, 350px) scale(1.1); }
|
|
80% { transform: translate(100px, 100px) scale(0.95); }
|
|
}
|
|
|
|
@keyframes drift-2 {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
20% { transform: translate(-250px, -150px) scale(1.1); }
|
|
40% { transform: translate(-100px, -400px) scale(0.95); }
|
|
60% { transform: translate(-300px, -200px) scale(1.15); }
|
|
80% { transform: translate(-50px, -300px) scale(0.9); }
|
|
}
|
|
|
|
@keyframes drift-3 {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
25% { transform: translate(-300px, -200px) scale(1.1); }
|
|
50% { transform: translate(-150px, 150px) scale(0.9); }
|
|
75% { transform: translate(-350px, -50px) scale(1.15); }
|
|
}
|
|
|
|
@keyframes drift-4 {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
25% { transform: translate(280px, -180px) scale(1.15); }
|
|
50% { transform: translate(100px, 200px) scale(0.9); }
|
|
75% { transform: translate(320px, 50px) scale(1.1); }
|
|
}
|
|
</style>
|