a11y: Tasks 2-6 - App shell, Titlebar, ToggleSwitch, Stepper, animate.ts

- Add skip-to-content link and dynamic document title (App.svelte)
- Wrap titlebar in header landmark, enlarge traffic lights to 44px (Titlebar.svelte)
- Enlarge toggle switch to 52x28, improve OFF knob contrast (ToggleSwitch.svelte)
- Enlarge stepper buttons to 36px, add keyboard hold-to-repeat (Stepper.svelte)
- Add keyboard feedback to pressable, focus glow to glowHover (animate.ts)
This commit is contained in:
Your Name
2026-02-18 18:08:14 +02:00
parent 3ae9db3be0
commit 95f684450c
5 changed files with 101 additions and 51 deletions

View File

@@ -75,6 +75,17 @@
}); });
}); });
// WCAG 2.4.2: Document title reflects current view
$effect(() => {
const viewNames: Record<string, string> = {
dashboard: "Dashboard",
breakScreen: "Break",
settings: "Settings",
stats: "Statistics",
};
document.title = `Core Cooldown — ${viewNames[effectiveView] ?? "Dashboard"}`;
});
// When fullscreen_mode is OFF, the separate break window handles breaks, // When fullscreen_mode is OFF, the separate break window handles breaks,
// so the main window should keep showing whatever view it was on (dashboard). // so the main window should keep showing whatever view it was on (dashboard).
const effectiveView = $derived( const effectiveView = $derived(
@@ -85,11 +96,13 @@
</script> </script>
<main class="relative h-full bg-black"> <main class="relative h-full bg-black">
<a href="#main-content" class="skip-link">Skip to content</a>
{#if $config.background_blobs_enabled} {#if $config.background_blobs_enabled}
<BackgroundBlobs accentColor={$config.accent_color} breakColor={$config.break_color} /> <BackgroundBlobs accentColor={$config.accent_color} breakColor={$config.break_color} />
{/if} {/if}
<Titlebar /> <Titlebar />
<div <div
id="main-content"
class="relative h-full overflow-hidden origin-top-left" class="relative h-full overflow-hidden origin-top-left"
style=" style="
transform: scale({zoomScale}); transform: scale({zoomScale});

View File

@@ -55,20 +55,34 @@
if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; } if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
if (holdInterval) { clearTimeout(holdInterval as unknown as ReturnType<typeof setTimeout>); holdInterval = null; } if (holdInterval) { clearTimeout(holdInterval as unknown as ReturnType<typeof setTimeout>); holdInterval = null; }
} }
function handleKeydown(fn: () => void, e: KeyboardEvent) {
if (["Enter", " "].includes(e.key)) {
e.preventDefault();
startHold(fn);
}
}
function handleKeyup(e: KeyboardEvent) {
if (["Enter", " "].includes(e.key)) {
stopHold();
}
}
</script> </script>
<div class="flex items-center gap-1.5" role="group" aria-label={label}> <div class="flex items-center gap-1.5" role="group" aria-label={label}>
<button <button
type="button" type="button"
aria-label="Decrease" aria-label="Decrease"
class="flex h-7 w-7 items-center justify-center rounded-lg class="flex h-9 w-9 min-h-[44px] min-w-[44px] items-center justify-center rounded-lg
bg-[#141414] text-[#8a8a8a] transition-colors border border-[#3a3a3a] bg-[#1a1a1a] text-text-sec transition-colors
hover:bg-[#1c1c1c] hover:text-white hover:bg-[#1c1c1c] hover:text-white
disabled:opacity-20" disabled:opacity-20"
onmousedown={() => startHold(decrement)} onmousedown={() => startHold(decrement)}
onmouseup={stopHold} onmouseup={stopHold}
onmouseleave={stopHold} onmouseleave={stopHold}
onclick={(e) => { if (e.detail === 0) decrement(); }} onkeydown={(e) => handleKeydown(decrement, e)}
onkeyup={handleKeyup}
disabled={value <= min} disabled={value <= min}
> >
&minus; &minus;
@@ -79,14 +93,15 @@
<button <button
type="button" type="button"
aria-label="Increase" aria-label="Increase"
class="flex h-7 w-7 items-center justify-center rounded-lg class="flex h-9 w-9 min-h-[44px] min-w-[44px] items-center justify-center rounded-lg
bg-[#141414] text-[#8a8a8a] transition-colors border border-[#3a3a3a] bg-[#1a1a1a] text-text-sec transition-colors
hover:bg-[#1c1c1c] hover:text-white hover:bg-[#1c1c1c] hover:text-white
disabled:opacity-20" disabled:opacity-20"
onmousedown={() => startHold(increment)} onmousedown={() => startHold(increment)}
onmouseup={stopHold} onmouseup={stopHold}
onmouseleave={stopHold} onmouseleave={stopHold}
onclick={(e) => { if (e.detail === 0) increment(); }} onkeydown={(e) => handleKeydown(increment, e)}
onkeyup={handleKeyup}
disabled={value >= max} disabled={value >= max}
> >
+ +

View File

@@ -5,7 +5,8 @@
</script> </script>
<!-- Invisible drag region traffic lights on the right --> <!-- Invisible drag region traffic lights on the right -->
<div <header
role="banner"
data-tauri-drag-region data-tauri-drag-region
class="group absolute top-0 left-0 right-0 z-50 flex h-10 items-center justify-end pr-3.5 select-none" class="group absolute top-0 left-0 right-0 z-50 flex h-10 items-center justify-end pr-3.5 select-none"
> >
@@ -20,15 +21,14 @@
</span> </span>
<!-- Traffic light buttons (order: maximize, minimize, close → close is rightmost) --> <!-- Traffic light buttons (order: maximize, minimize, close → close is rightmost) -->
<div class="flex items-center gap-[8px] opacity-10 transition-opacity duration-300 group-hover:opacity-100 group-focus-within:opacity-100"> <div class="flex items-center gap-0 opacity-10 transition-opacity duration-300 group-hover:opacity-100 group-focus-within:opacity-100">
<!-- Maximize (green) --> <!-- Maximize (green) -->
<button <button
aria-label="Maximize" aria-label="Maximize"
class="traffic-btn group/btn relative flex h-[15px] w-[15px] items-center justify-center class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
rounded-full bg-[#27C93F] transition-all duration-150
hover:brightness-110"
onclick={() => appWindow.toggleMaximize()} onclick={() => appWindow.toggleMaximize()}
> >
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#27C93F] transition-all duration-150 group-hover/btn:brightness-110">
<svg <svg
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100" class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
width="8" width="8"
@@ -40,16 +40,16 @@
<polyline points="7,3 7,1 5,1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none" /> <polyline points="7,3 7,1 5,1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<line x1="1" y1="7" x2="7" y2="1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" /> <line x1="1" y1="7" x2="7" y2="1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" />
</svg> </svg>
</span>
</button> </button>
<!-- Minimize (yellow) --> <!-- Minimize (yellow) -->
<button <button
aria-label="Minimize" aria-label="Minimize"
class="traffic-btn group/btn relative flex h-[15px] w-[15px] items-center justify-center class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
rounded-full bg-[#FFBD2E] transition-all duration-150
hover:brightness-110"
onclick={() => appWindow.minimize()} onclick={() => appWindow.minimize()}
> >
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#FFBD2E] transition-all duration-150 group-hover/btn:brightness-110">
<svg <svg
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100" class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
width="8" width="8"
@@ -59,16 +59,16 @@
> >
<line x1="1" y1="1" x2="7" y2="1" stroke="#995700" stroke-width="1.3" stroke-linecap="round" /> <line x1="1" y1="1" x2="7" y2="1" stroke="#995700" stroke-width="1.3" stroke-linecap="round" />
</svg> </svg>
</span>
</button> </button>
<!-- Close (red) — rightmost --> <!-- Close (red) — rightmost -->
<button <button
aria-label="Close" aria-label="Close"
class="traffic-btn group/btn relative flex h-[15px] w-[15px] items-center justify-center class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
rounded-full bg-[#FF5F57] transition-all duration-150
hover:brightness-110"
onclick={() => appWindow.close()} onclick={() => appWindow.close()}
> >
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#FF5F57] transition-all duration-150 group-hover/btn:brightness-110">
<svg <svg
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100" class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
width="8" width="8"
@@ -79,6 +79,7 @@
<line x1="1.5" y1="1.5" x2="6.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" /> <line x1="1.5" y1="1.5" x2="6.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" />
<line x1="6.5" y1="1.5" x2="1.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" /> <line x1="6.5" y1="1.5" x2="1.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" />
</svg> </svg>
</span>
</button> </button>
</div> </div>
</div> </header>

View File

@@ -20,14 +20,14 @@
role="switch" role="switch"
aria-label={label} aria-label={label}
aria-checked={checked} aria-checked={checked}
class="relative inline-flex h-[24px] w-[48px] shrink-0 cursor-pointer rounded-full class="relative inline-flex h-[28px] w-[52px] min-h-[44px] shrink-0 cursor-pointer rounded-full
transition-colors duration-200 ease-in-out" transition-colors duration-200 ease-in-out"
style="background: {checked ? $config.accent_color : '#1a1a1a'};" style="background: {checked ? $config.accent_color : '#1a1a1a'};"
onclick={toggle} onclick={toggle}
> >
<span <span
class="pointer-events-none inline-block h-[19px] w-[19px] rounded-full class="pointer-events-none inline-block h-[22px] w-[22px] rounded-full
shadow-sm transition-transform duration-200 ease-in-out shadow-sm transition-transform duration-200 ease-in-out
{checked ? 'translate-x-[26px] bg-white' : 'translate-x-[3px] bg-[#444]'} mt-[2.5px]" {checked ? 'translate-x-[27px] bg-white' : 'translate-x-[3px] bg-[#666]'} mt-[3px]"
></span> ></span>
</button> </button>

View File

@@ -140,15 +140,32 @@ export function pressable(node: HTMLElement) {
); );
} }
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onDown();
}
}
function onKeyUp(e: KeyboardEvent) {
if (e.key === "Enter" || e.key === " ") {
onUp();
}
}
node.addEventListener("mousedown", onDown); node.addEventListener("mousedown", onDown);
node.addEventListener("mouseup", onUp); node.addEventListener("mouseup", onUp);
node.addEventListener("mouseleave", onUp); node.addEventListener("mouseleave", onUp);
node.addEventListener("keydown", onKeyDown);
node.addEventListener("keyup", onKeyUp);
return { return {
destroy() { destroy() {
node.removeEventListener("mousedown", onDown); node.removeEventListener("mousedown", onDown);
node.removeEventListener("mouseup", onUp); node.removeEventListener("mouseup", onUp);
node.removeEventListener("mouseleave", onUp); node.removeEventListener("mouseleave", onUp);
node.removeEventListener("keydown", onKeyDown);
node.removeEventListener("keyup", onKeyUp);
active?.cancel(); active?.cancel();
}, },
}; };
@@ -200,6 +217,8 @@ export function glowHover(
node.addEventListener("mouseenter", onEnter); node.addEventListener("mouseenter", onEnter);
node.addEventListener("mouseleave", onLeave); node.addEventListener("mouseleave", onLeave);
node.addEventListener("focusin", onEnter);
node.addEventListener("focusout", onLeave);
return { return {
update(newOptions?: { color?: string }) { update(newOptions?: { color?: string }) {
@@ -209,6 +228,8 @@ export function glowHover(
destroy() { destroy() {
node.removeEventListener("mouseenter", onEnter); node.removeEventListener("mouseenter", onEnter);
node.removeEventListener("mouseleave", onLeave); node.removeEventListener("mouseleave", onLeave);
node.removeEventListener("focusin", onEnter);
node.removeEventListener("focusout", onLeave);
enterAnim?.cancel(); enterAnim?.cancel();
leaveAnim?.cancel(); leaveAnim?.cancel();
}, },