mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 06:26:42 +02:00
♿️ fix: resolve all svelte-check a11y warnings across web apps
- Fix 121 accessibility warnings across 9 web apps (manacore, clock, chat, manadeck, calendar, zitare, contacts, picture, todo) - Add proper ARIA attributes (role, tabindex, aria-label) to interactive elements - Add onkeydown handlers alongside onclick for keyboard accessibility - Add svelte-ignore comments for intentional patterns (modals, dropdowns) - Update svelte-check threshold from error to warning in pre-commit hook - Fix script compatibility for bash 3.x (remove associative arrays) - Add comprehensive documentation for svelte-check patterns and fixes All web apps now pass svelte-check with 0 errors and 0 warnings. Pre-commit hooks will block any future commits with warnings.
This commit is contained in:
parent
b949037fa5
commit
42e5e97390
101 changed files with 1048 additions and 558 deletions
|
|
@ -23,6 +23,7 @@
|
|||
let saving = $state(false);
|
||||
let deleting = $state(false);
|
||||
let uploadingPhoto = $state(false);
|
||||
// svelte-ignore non_reactive_update - Element reference doesn't need reactivity
|
||||
let photoInput: HTMLInputElement;
|
||||
|
||||
// Edit form state
|
||||
|
|
@ -1089,15 +1090,6 @@
|
|||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.spinner-lg {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
|
|
@ -1105,11 +1097,6 @@
|
|||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.error-container {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
// Infinite scroll
|
||||
let intersectionObserver: IntersectionObserver | null = null;
|
||||
// svelte-ignore non_reactive_update - Element reference doesn't need reactivity
|
||||
let loadMoreTrigger: HTMLDivElement;
|
||||
|
||||
// Batch selection state
|
||||
|
|
|
|||
|
|
@ -445,12 +445,6 @@
|
|||
}
|
||||
|
||||
/* Loading & Empty */
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
|
|
|||
|
|
@ -157,9 +157,10 @@
|
|||
>
|
||||
<!-- Tags Filter -->
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.tag')}</label>
|
||||
<span class="filter-label" id="tag-filter-label">{$_('filters.tag')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="tag-filter-label"
|
||||
value={selectedTagId || ''}
|
||||
onchange={(e) => onTagChange(e.currentTarget.value || null)}
|
||||
>
|
||||
|
|
@ -172,9 +173,10 @@
|
|||
|
||||
<!-- Contact Info Filter -->
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.contactInfo')}</label>
|
||||
<span class="filter-label" id="contact-filter-label">{$_('filters.contactInfo')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="contact-filter-label"
|
||||
value={contactFilter}
|
||||
onchange={(e) => onContactFilterChange(e.currentTarget.value as ContactFilter)}
|
||||
>
|
||||
|
|
@ -188,9 +190,10 @@
|
|||
|
||||
<!-- Birthday Filter -->
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.birthdayLabel')}</label>
|
||||
<span class="filter-label" id="birthday-filter-label">{$_('filters.birthdayLabel')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="birthday-filter-label"
|
||||
value={birthdayFilter}
|
||||
onchange={(e) => onBirthdayFilterChange(e.currentTarget.value as BirthdayFilter)}
|
||||
>
|
||||
|
|
@ -204,9 +207,10 @@
|
|||
<!-- Company Filter -->
|
||||
{#if companies.length > 0}
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.company')}</label>
|
||||
<span class="filter-label" id="company-filter-label">{$_('filters.company')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="company-filter-label"
|
||||
value={selectedCompany || ''}
|
||||
onchange={(e) => onCompanyChange(e.currentTarget.value || null)}
|
||||
>
|
||||
|
|
@ -320,9 +324,10 @@
|
|||
<div class="filter-panel">
|
||||
<!-- Tags Filter -->
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.tag')}</label>
|
||||
<span class="filter-label" id="tag-filter-label">{$_('filters.tag')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="tag-filter-label"
|
||||
value={selectedTagId || ''}
|
||||
onchange={(e) => onTagChange(e.currentTarget.value || null)}
|
||||
>
|
||||
|
|
@ -335,9 +340,10 @@
|
|||
|
||||
<!-- Contact Info Filter -->
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.contactInfo')}</label>
|
||||
<span class="filter-label" id="contact-filter-label">{$_('filters.contactInfo')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="contact-filter-label"
|
||||
value={contactFilter}
|
||||
onchange={(e) => onContactFilterChange(e.currentTarget.value as ContactFilter)}
|
||||
>
|
||||
|
|
@ -351,9 +357,10 @@
|
|||
|
||||
<!-- Birthday Filter -->
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.birthdayLabel')}</label>
|
||||
<span class="filter-label" id="birthday-filter-label">{$_('filters.birthdayLabel')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="birthday-filter-label"
|
||||
value={birthdayFilter}
|
||||
onchange={(e) => onBirthdayFilterChange(e.currentTarget.value as BirthdayFilter)}
|
||||
>
|
||||
|
|
@ -367,9 +374,10 @@
|
|||
<!-- Company Filter -->
|
||||
{#if companies.length > 0}
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">{$_('filters.company')}</label>
|
||||
<span class="filter-label" id="company-filter-label">{$_('filters.company')}</span>
|
||||
<select
|
||||
class="filter-select"
|
||||
aria-labelledby="company-filter-label"
|
||||
value={selectedCompany || ''}
|
||||
onchange={(e) => onCompanyChange(e.currentTarget.value || null)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
let loading = $state(false);
|
||||
let selectedIndex = $state(0);
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
// svelte-ignore non_reactive_update - Element reference doesn't need reactivity
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
// Reset state when modal opens
|
||||
|
|
@ -109,12 +110,13 @@
|
|||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_interactive_supports_focus -->
|
||||
<div
|
||||
class="search-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Kontakt suchen"
|
||||
tabindex="-1"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -49,10 +49,14 @@
|
|||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="bg-card rounded-xl shadow-xl w-full max-w-md p-6 space-y-6">
|
||||
<!-- Header -->
|
||||
|
|
@ -62,6 +66,7 @@
|
|||
type="button"
|
||||
onclick={onClose}
|
||||
class="text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label={$_('common.close')}
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -92,8 +97,10 @@
|
|||
|
||||
<!-- Format Selection -->
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-foreground">{$_('export.format')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<span class="block text-sm font-medium text-foreground" id="format-label"
|
||||
>{$_('export.format')}</span
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3" role="group" aria-labelledby="format-label">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (format = 'vcard')}
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@
|
|||
export { resetZoom, zoomIn, zoomOut };
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
bind:this={containerElement}
|
||||
class="network-graph-container"
|
||||
|
|
@ -253,6 +254,7 @@
|
|||
{@const isSelected = node.id === networkStore.selectedNodeId}
|
||||
{@const isConnected = isConnectedToSelected(node.id, graphLinks)}
|
||||
{@const isDimmed = networkStore.selectedNodeId && !isConnected}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<g
|
||||
transform="translate({node.x ?? 0}, {node.y ?? 0})"
|
||||
class="node"
|
||||
|
|
@ -262,6 +264,7 @@
|
|||
onmousedown={(e) => handleDragStart(e, node)}
|
||||
onclick={() => handleNodeClick(node)}
|
||||
ondblclick={() => handleNodeDoubleClick(node)}
|
||||
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && handleNodeClick(node)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={node.name}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
previousNodeCount = currentNodeCount;
|
||||
});
|
||||
|
||||
// svelte-ignore non_reactive_update - Component reference doesn't need reactivity
|
||||
let graphComponent: NetworkGraph;
|
||||
let graphContainer: HTMLDivElement;
|
||||
|
||||
|
|
|
|||
|
|
@ -404,28 +404,6 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: 3px solid hsl(var(--color-muted));
|
||||
border-top-color: hsl(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue