fix(a11y): resolve 30 svelte-check warnings across 8 files

svelte-check now completes clean (0 errors, 0 warnings, 0 files with
problems).

- profile/ContextOverview: 11 click-on-div sites made keyboard-
  accessible with role="button", tabindex="0", and an onActivate helper
  that fires the same handler on Enter/Space. Two <p> wrappers became
  <div> since <p> cannot carry role="button" per ARIA.
- profile/ContextInterview: paginate dots got aria-label + aria-current.
- settings/GeneralSection: toggle button got aria-label +
  aria-pressed.
- events/RegionPicker: radius label associated with range input via
  for/id.
- events/SourceManager: drop unused .source-item.inactive + .inactive-
  badge CSS selectors (dead code).
- research-lab/CompareColumn: local `rating` seed from entry.userRating
  now uses svelte-ignore comment + $effect sync (intentional seed-only
  read, plus prop-update mirror).
- admin/ListView: initialTab prop is deliberately read only at mount;
  svelte-ignore comment documents the intent.
- gifts/redeem: drop unused .animate-fade-in CSS selector.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-22 17:00:59 +02:00
parent 9db044178c
commit 3e09ff66d1
8 changed files with 90 additions and 24 deletions

View file

@ -135,6 +135,8 @@
class="toggle"
class:on={userSettings.general?.soundsEnabled ?? true}
onclick={() => setSounds(!(userSettings.general?.soundsEnabled ?? true))}
aria-label="Sounds ein- oder ausschalten"
aria-pressed={userSettings.general?.soundsEnabled ?? true}
>
<span class="toggle-knob"></span>
</button>

View file

@ -26,6 +26,9 @@
let { initialTab = 'overview' }: Props = $props();
// initialTab is an entry-point default only — we deliberately ignore later prop
// changes because the user may have switched tabs since mount.
// svelte-ignore state_referenced_locally
let activeTab = $state<TabId>(initialTab);
let isAdmin = $derived(authStore.user?.role === 'admin');

View file

@ -88,8 +88,8 @@
placeholder="Stadt oder Region suchen..."
/>
<div class="radius-row">
<label class="radius-label">Radius: {radiusKm} km</label>
<input type="range" min="5" max="100" step="5" bind:value={radiusKm} />
<label class="radius-label" for="region-radius">Radius: {radiusKm} km</label>
<input id="region-radius" type="range" min="5" max="100" step="5" bind:value={radiusKm} />
</div>
{#if suggestions.length > 0}
<ul class="suggestions">

View file

@ -314,9 +314,6 @@
.source-item.error {
border-color: rgba(239, 68, 68, 0.3);
}
.source-item.inactive {
opacity: 0.5;
}
.source-info {
flex: 1;
min-width: 0;
@ -342,14 +339,6 @@
background: rgba(239, 68, 68, 0.15);
color: rgb(220, 38, 38);
}
.inactive-badge {
font-size: 0.625rem;
font-weight: 600;
padding: 0.0625rem 0.375rem;
border-radius: 0.25rem;
background: hsl(var(--color-muted));
color: hsl(var(--color-muted-foreground));
}
.source-error {
font-size: 0.6875rem;
color: rgb(220, 38, 38);

View file

@ -562,6 +562,8 @@
class:active={i === currentQuestionIdx}
class:answered={answeredSet.has(categoryQuestions[i].id)}
onclick={() => (currentQuestionIdx = i)}
aria-label="Frage {i + 1}"
aria-current={i === currentQuestionIdx ? 'step' : undefined}
></button>
{/each}
</div>

View file

@ -65,6 +65,18 @@
5: 'Fr',
6: 'Sa',
};
// Enter / Space on a non-button click-target counts as activation, same
// as a button would. Used to make the "tap a section to edit" surfaces
// keyboard-accessible without rewriting the whole card layout.
function onActivate(handler: () => void) {
return (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handler();
}
};
}
</script>
<div class="overview">
@ -108,9 +120,15 @@
<button class="edit-btn primary" onclick={() => saveEdit('about.bio')}>Speichern</button
>
</div>
{:else}<p class="section-text" onclick={() => startEdit('about.bio', ctx?.about?.bio)}>
{:else}<div
class="section-text"
role="button"
tabindex="0"
onclick={() => startEdit('about.bio', ctx?.about?.bio)}
onkeydown={onActivate(() => startEdit('about.bio', ctx?.about?.bio))}
>
{ctx?.about?.bio}
</p>{/if}
</div>{/if}
</section>
{/if}
@ -144,7 +162,13 @@
</div>
</div>
{:else if ctx?.interests?.length}
<div class="tags-list" onclick={() => startEdit('interests', ctx?.interests ?? [])}>
<div
class="tags-list"
role="button"
tabindex="0"
onclick={() => startEdit('interests', ctx?.interests ?? [])}
onkeydown={onActivate(() => startEdit('interests', ctx?.interests ?? []))}
>
{#each ctx.interests as tag (tag)}<span class="tag">{tag}</span>{/each}
</div>
{:else}
@ -162,7 +186,13 @@
<section class="section-card">
<h3 class="section-title">Tagesablauf</h3>
{#if ctx?.routine && (ctx.routine.wakeUp || ctx.routine.workStart || ctx.routine.bedtime)}
<div class="routine-grid" onclick={() => onStartInterview()}>
<div
class="routine-grid"
role="button"
tabindex="0"
onclick={() => onStartInterview()}
onkeydown={onActivate(() => onStartInterview())}
>
{#if ctx.routine.wakeUp}<div class="routine-item">
<span class="routine-label">Aufstehen</span><span class="routine-value"
>{ctx.routine.wakeUp}</span
@ -196,9 +226,15 @@
<h3 class="section-title">Ernährung</h3>
{#if ctx?.nutrition && (ctx.nutrition.diet || ctx.nutrition.allergies?.length)}
<div>
{#if ctx.nutrition.diet}<p class="section-text" onclick={() => onStartInterview()}>
{#if ctx.nutrition.diet}<div
class="section-text"
role="button"
tabindex="0"
onclick={() => onStartInterview()}
onkeydown={onActivate(() => onStartInterview())}
>
{ctx.nutrition.diet}
</p>{/if}
</div>{/if}
{#if ctx.nutrition.allergies?.length}
{#if editingField === 'nutrition.allergies'}
<div class="tags-edit">
@ -232,7 +268,12 @@
{:else}
<div
class="tags-list"
role="button"
tabindex="0"
onclick={() => startEdit('nutrition.allergies', ctx?.nutrition?.allergies ?? [])}
onkeydown={onActivate(() =>
startEdit('nutrition.allergies', ctx?.nutrition?.allergies ?? [])
)}
>
{#each ctx.nutrition.allergies as a (a)}<span class="tag warning">{a}</span>{/each}
</div>
@ -258,7 +299,10 @@
<span class="routine-label">Sport</span>
<div
class="tags-list"
role="button"
tabindex="0"
onclick={() => startEdit('leisure.sports', ctx?.leisure?.sports ?? [])}
onkeydown={onActivate(() => startEdit('leisure.sports', ctx?.leisure?.sports ?? []))}
>
{#each ctx.leisure.sports as s (s)}<span class="tag">{s}</span>{/each}
</div>
@ -269,7 +313,10 @@
<span class="routine-label">Medien</span>
<div
class="tags-list"
role="button"
tabindex="0"
onclick={() => startEdit('leisure.media', ctx?.leisure?.media ?? [])}
onkeydown={onActivate(() => startEdit('leisure.media', ctx?.leisure?.media ?? []))}
>
{#each ctx.leisure.media as m (m)}<span class="tag">{m}</span>{/each}
</div>
@ -280,7 +327,10 @@
<span class="routine-label">Haustiere</span>
<span
class="section-text"
role="button"
tabindex="0"
onclick={() => startEdit('leisure.pets', ctx?.leisure?.pets ?? '')}
onkeydown={onActivate(() => startEdit('leisure.pets', ctx?.leisure?.pets ?? ''))}
>{ctx.leisure.pets}</span
>
</div>
@ -318,7 +368,13 @@
</div>
</div>
{:else if ctx?.goals?.length}
<div class="tags-list" onclick={() => startEdit('goals', ctx?.goals ?? [])}>
<div
class="tags-list"
role="button"
tabindex="0"
onclick={() => startEdit('goals', ctx?.goals ?? [])}
onkeydown={onActivate(() => startEdit('goals', ctx?.goals ?? []))}
>
{#each ctx.goals as goal (goal)}<span class="tag accent">{goal}</span>{/each}
</div>
{:else}
@ -336,7 +392,13 @@
<section class="section-card">
<h3 class="section-title">Arbeitsstil</h3>
{#if ctx?.social && (ctx.social.workStyle || ctx.social.communication || ctx.social.livingSetup)}
<div class="routine-grid" onclick={() => onStartInterview()}>
<div
class="routine-grid"
role="button"
tabindex="0"
onclick={() => onStartInterview()}
onkeydown={onActivate(() => onStartInterview())}
>
{#if ctx.social.workStyle}<div class="routine-item">
<span class="routine-label">Arbeitsweise</span><span class="routine-value"
>{ctx.social.workStyle}</span
@ -394,7 +456,10 @@
{:else if ctx?.about?.languages?.length}
<div
class="tags-list"
role="button"
tabindex="0"
onclick={() => startEdit('about.languages', ctx?.about?.languages ?? [])}
onkeydown={onActivate(() => startEdit('about.languages', ctx?.about?.languages ?? []))}
>
{#each ctx.about.languages as lang (lang)}<span class="tag">{lang}</span>{/each}
</div>

View file

@ -20,9 +20,17 @@
const { category, entry, runId }: Props = $props();
// Local optimistic rating — seed from the current entry, then keep in sync
// via $effect when the parent swaps the prop. The seed-only read of
// entry.userRating is intentional; the $effect covers prop updates.
// svelte-ignore state_referenced_locally
let rating = $state(entry.userRating ?? 0);
let ratingError = $state<string | null>(null);
$effect(() => {
rating = entry.userRating ?? 0;
});
async function setRating(value: number) {
if (!runId || !entry.resultId) return;
ratingError = null;

View file

@ -294,9 +294,6 @@
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
@keyframes bounce-once {
0%,
100% {