fix: resolve all linter errors and a11y warnings
CI / test (push) Has been cancelled
CI / docker-build (push) Has been cancelled
CI / lint-and-check (push) Has been cancelled

- Fix TS errors: editMode property order, implicit any, string|undefined
- Add $state() to bind:this element refs (IconGrid, EntityPicker, etc.)
- Fix a11y: labels, aria-labels, roles, tabindex on dialogs
- Remove unused imports (tick, svelte-i18n)
- Make AutocompleteInput/TagsInput accept optional string values
This commit is contained in:
2026-04-10 19:05:25 +03:00
parent f96cbbca56
commit 44e1849821
20 changed files with 59 additions and 33 deletions
@@ -133,6 +133,7 @@
<h3 class="mb-3 text-sm font-semibold text-card-foreground">{$t('admin.perm_title')}</h3> <h3 class="mb-3 text-sm font-semibold text-card-foreground">{$t('admin.perm_title')}</h3>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-5"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-5">
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_entity_type')}</label> <label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_entity_type')}</label>
<IconGrid <IconGrid
items={entityTypeItems} items={entityTypeItems}
@@ -142,6 +143,7 @@
/> />
</div> </div>
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_entity')}</label> <label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_entity')}</label>
<EntityPicker <EntityPicker
items={entityPickerItems} items={entityPickerItems}
@@ -151,6 +153,7 @@
/> />
</div> </div>
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_target_type')}</label> <label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_target_type')}</label>
<IconGrid <IconGrid
items={targetTypeItems} items={targetTypeItems}
@@ -160,6 +163,7 @@
/> />
</div> </div>
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_target')}</label> <label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_target')}</label>
<EntityPicker <EntityPicker
items={targetPickerItems} items={targetPickerItems}
@@ -169,6 +173,7 @@
/> />
</div> </div>
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_level')}</label> <label class="mb-1 block text-xs text-muted-foreground">{$t('admin.perm_level')}</label>
<div class="flex gap-1"> <div class="flex gap-1">
<div class="flex-1"> <div class="flex-1">
+1
View File
@@ -227,6 +227,7 @@
{#if $form.healthcheckEnabled} {#if $form.healthcheckEnabled}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label <label
class="mb-1 block text-sm font-medium text-card-foreground" class="mb-1 block text-sm font-medium text-card-foreground"
> >
@@ -39,6 +39,7 @@
</script> </script>
<div class="space-y-2"> <div class="space-y-2">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block text-sm font-medium text-card-foreground">{$t('app.icon')}</label> <label class="block text-sm font-medium text-card-foreground">{$t('app.icon')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -144,6 +144,7 @@
<button <button
type="button" type="button"
onclick={() => removeLink(link.id)} onclick={() => removeLink(link.id)}
aria-label="Remove link"
class="flex-shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive" class="flex-shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
> >
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -91,6 +91,7 @@
<button <button
type="button" type="button"
onclick={onClose} onclick={onClose}
aria-label="Close"
class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -111,6 +112,7 @@
<!-- Icon --> <!-- Icon -->
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground">{$t('app.icon') ?? 'Icon'}</label> <label class="mb-1 block text-sm font-medium text-foreground">{$t('app.icon') ?? 'Icon'}</label>
<IconPickerButton value={icon} onchange={(v) => { icon = v; }} /> <IconPickerButton value={icon} onchange={(v) => { icon = v; }} />
</div> </div>
@@ -148,10 +148,11 @@
<svelte:window onkeydown={handleKeydown} /> <svelte:window onkeydown={handleKeydown} />
<!-- Backdrop --> <!-- Backdrop -->
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div <div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onclick={handleBackdropClick} onclick={handleBackdropClick}
role="presentation"
> >
<div class="mx-4 w-full max-w-lg rounded-xl border border-border bg-card p-6 shadow-xl" role="dialog" aria-modal="true"> <div class="mx-4 w-full max-w-lg rounded-xl border border-border bg-card p-6 shadow-xl" role="dialog" aria-modal="true">
<!-- Header --> <!-- Header -->
@@ -4,8 +4,8 @@
import SearchResult from './SearchResult.svelte'; import SearchResult from './SearchResult.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement = $state() as HTMLInputElement;
let resultsEl: HTMLDivElement; let resultsEl: HTMLDivElement = $state() as HTMLDivElement;
$effect(() => { $effect(() => {
if (search.open && inputEl) { if (search.open && inputEl) {
@@ -68,10 +68,11 @@
</script> </script>
{#if search.open} {#if search.open}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div <div
class="fixed inset-0 z-50 flex items-start justify-center bg-black/50 pt-[15vh] backdrop-blur-sm" class="fixed inset-0 z-50 flex items-start justify-center bg-black/50 pt-[15vh] backdrop-blur-sm"
onclick={handleBackdropClick} onclick={handleBackdropClick}
role="presentation"
style="animation: searchFadeIn 0.15s ease-out" style="animation: searchFadeIn 0.15s ease-out"
> >
<!-- Dialog --> <!-- Dialog -->
@@ -80,6 +80,7 @@
<div class="space-y-3"> <div class="space-y-3">
{#if label} {#if label}
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block text-sm font-medium text-foreground">{label}</label> <label class="block text-sm font-medium text-foreground">{label}</label>
{/if} {/if}
@@ -154,6 +154,7 @@
<!-- Hue slider --> <!-- Hue slider -->
<div class="mb-4"> <div class="mb-4">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1.5 block text-sm text-muted-foreground">{$t('settings.hue')}</label> <label class="mb-1.5 block text-sm text-muted-foreground">{$t('settings.hue')}</label>
<div class="relative"> <div class="relative">
<div <div
@@ -174,7 +175,8 @@
<!-- Saturation slider --> <!-- Saturation slider -->
<div> <div>
<label class="mb-1.5 block text-sm text-muted-foreground">{$t('settings.saturation')}</label> <!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1.5 block text-sm text-muted-foreground">{$t('settings.saturation')}</label>
<div class="relative"> <div class="relative">
<div <div
class="h-3 w-full rounded-full" class="h-3 w-full rounded-full"
@@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { tick } from 'svelte';
interface Props { interface Props {
id?: string; id?: string;
name?: string; name?: string;
value: string; value: string | undefined;
suggestions: string[]; suggestions: string[];
placeholder?: string; placeholder?: string;
class?: string; class?: string;
@@ -25,7 +23,7 @@
let containerEl: HTMLDivElement | undefined = $state(); let containerEl: HTMLDivElement | undefined = $state();
const filtered = $derived.by(() => { const filtered = $derived.by(() => {
const q = value.trim().toLowerCase(); const q = (value ?? '').trim().toLowerCase();
if (!q) return suggestions; if (!q) return suggestions;
return suggestions.filter((s) => s.toLowerCase().includes(q)); return suggestions.filter((s) => s.toLowerCase().includes(q));
}); });
@@ -46,6 +46,7 @@
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
transition:scale={{ start: 0.95, duration: 150 }} transition:scale={{ start: 0.95, duration: 150 }}
role="alertdialog" role="alertdialog"
tabindex="-1"
aria-labelledby="confirm-dialog-title" aria-labelledby="confirm-dialog-title"
aria-describedby="confirm-dialog-message" aria-describedby="confirm-dialog-message"
> >
+2 -2
View File
@@ -34,8 +34,8 @@
let open = $state(false); let open = $state(false);
let query = $state(''); let query = $state('');
let highlightIdx = $state(0); let highlightIdx = $state(0);
let listEl: HTMLDivElement; let listEl = $state<HTMLDivElement>();
let inputEl: HTMLInputElement; let inputEl = $state<HTMLInputElement>();
const selectedItem = $derived(items.find((i) => i.value === value)); const selectedItem = $derived(items.find((i) => i.value === value));
+3 -2
View File
@@ -21,8 +21,8 @@
$props(); $props();
let open = $state(false); let open = $state(false);
let triggerEl: HTMLButtonElement; let triggerEl = $state<HTMLButtonElement>();
let popupEl: HTMLDivElement; let popupEl = $state<HTMLDivElement>();
let filterQuery = $state(''); let filterQuery = $state('');
const selectedItem = $derived(items.find((i) => i.value === value)); const selectedItem = $derived(items.find((i) => i.value === value));
@@ -135,6 +135,7 @@
> >
{#if showFilter} {#if showFilter}
<div class="border-b border-border px-3 py-2"> <div class="border-b border-border px-3 py-2">
<!-- svelte-ignore a11y_autofocus -->
<input <input
type="text" type="text"
bind:value={filterQuery} bind:value={filterQuery}
+4 -6
View File
@@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { t } from 'svelte-i18n';
interface Props { interface Props {
id?: string; id?: string;
name?: string; name?: string;
value: string; value: string | undefined;
suggestions: string[]; suggestions: string[];
placeholder?: string; placeholder?: string;
class?: string; class?: string;
@@ -25,11 +23,11 @@
let containerEl: HTMLDivElement | undefined = $state(); let containerEl: HTMLDivElement | undefined = $state();
// Parse comma-separated tags // Parse comma-separated tags
const tags = $derived(value.split(',').map((t) => t.trim()).filter(Boolean)); const tags = $derived((value ?? '').split(',').map((t) => t.trim()).filter(Boolean));
// Get the current partial tag being typed (after last comma) // Get the current partial tag being typed (after last comma)
const currentPartial = $derived.by(() => { const currentPartial = $derived.by(() => {
const parts = value.split(','); const parts = (value ?? '').split(',');
return parts[parts.length - 1]?.trim() ?? ''; return parts[parts.length - 1]?.trim() ?? '';
}); });
@@ -53,7 +51,7 @@
function selectItem(item: string) { function selectItem(item: string) {
// Replace the current partial with the selected tag // Replace the current partial with the selected tag
const parts = value.split(',').map((p) => p.trim()); const parts = (value ?? '').split(',').map((p) => p.trim());
parts[parts.length - 1] = item; parts[parts.length - 1] = item;
value = parts.join(',') + ','; value = parts.join(',') + ',';
open = false; open = false;
@@ -196,6 +196,7 @@
{/if} {/if}
{:else if cardSize === 'large'} {:else if cardSize === 'large'}
<!-- Large: icon + name + description + sparkline + tags + links --> <!-- Large: icon + name + description + sparkline + tags + links -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="card-hover group rounded-xl {cardStyleClass} p-5 transition-colors hover:border-primary/50" class="card-hover group rounded-xl {cardStyleClass} p-5 transition-colors hover:border-primary/50"
data-app-widget data-app-widget
@@ -291,6 +292,7 @@
</div> </div>
{:else} {:else}
<!-- Medium (default): icon + name + status + sparkline on hover + links --> <!-- Medium (default): icon + name + status + sparkline on hover + links -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="card-hover group rounded-xl {cardStyleClass} p-4 transition-colors hover:border-primary/50" class="card-hover group rounded-xl {cardStyleClass} p-4 transition-colors hover:border-primary/50"
data-app-widget data-app-widget
@@ -363,6 +363,7 @@
<div class="mb-3 rounded-lg border border-border bg-muted/50 p-3"> <div class="mb-3 rounded-lg border border-border bg-muted/50 p-3">
<!-- Widget Type Selector (Icon Grid) --> <!-- Widget Type Selector (Icon Grid) -->
<div class="mb-3"> <div class="mb-3">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground"> <label class="mb-1 block text-sm font-medium text-foreground">
Widget Type Widget Type
</label> </label>
@@ -376,6 +377,7 @@
<!-- Type-specific config forms --> <!-- Type-specific config forms -->
{#if selectedWidgetType === 'app'} {#if selectedWidgetType === 'app'}
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground"> <label class="mb-1 block text-sm font-medium text-foreground">
{$t('widget.select_app')} {$t('widget.select_app')}
</label> </label>
@@ -434,6 +436,7 @@
{:else if selectedWidgetType === 'note'} {:else if selectedWidgetType === 'note'}
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground">Format</label> <label class="mb-1 block text-sm font-medium text-foreground">Format</label>
<IconGrid <IconGrid
items={noteFormatItems} items={noteFormatItems}
@@ -516,6 +519,7 @@
{:else if selectedWidgetType === 'clock'} {:else if selectedWidgetType === 'clock'}
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground">Clock Style</label> <label class="mb-1 block text-sm font-medium text-foreground">Clock Style</label>
<IconGrid <IconGrid
items={clockStyleItems} items={clockStyleItems}
@@ -760,6 +764,7 @@
/> />
</div> </div>
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground">Source Type</label> <label class="mb-1 block text-sm font-medium text-foreground">Source Type</label>
<IconGrid <IconGrid
items={metricSourceItems} items={metricSourceItems}
+15 -11
View File
@@ -5,7 +5,7 @@
import type { LayoutData } from './$types.js'; import type { LayoutData } from './$types.js';
import MainLayout from '$lib/components/layout/MainLayout.svelte'; import MainLayout from '$lib/components/layout/MainLayout.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { fade } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { theme } from '$lib/stores/theme.svelte'; import { theme } from '$lib/stores/theme.svelte';
import { ui } from '$lib/stores/ui.svelte'; import { ui } from '$lib/stores/ui.svelte';
import { search } from '$lib/stores/search.svelte'; import { search } from '$lib/stores/search.svelte';
@@ -22,12 +22,14 @@
let { data, children }: { data: LayoutData; children: Snippet } = $props(); let { data, children }: { data: LayoutData; children: Snippet } = $props();
// Apply user preferences from server (overrides localStorage defaults) // Apply user preferences from server (overrides localStorage defaults)
if (data.userPreferences) { $effect(() => {
theme.loadFromServer(data.userPreferences); if (data.userPreferences) {
if (data.userPreferences.locale) { theme.loadFromServer(data.userPreferences);
i18nLocale.set(data.userPreferences.locale); if (data.userPreferences.locale) {
i18nLocale.set(data.userPreferences.locale);
}
} }
} });
// Initialize store effects within component context // Initialize store effects within component context
theme.initEffects(); theme.initEffects();
@@ -38,9 +40,11 @@
keyboard.init(); keyboard.init();
// Load favorites for authenticated users // Load favorites for authenticated users
if (data.user) { $effect(() => {
favorites.load(); if (data.user) {
} favorites.load();
}
});
// Listen for cross-tab sync messages (theme changes & data invalidation) // Listen for cross-tab sync messages (theme changes & data invalidation)
const cleanupSync = onSyncMessage((msg) => { const cleanupSync = onSyncMessage((msg) => {
@@ -82,14 +86,14 @@
boards={data.sidebarBoards ?? []} boards={data.sidebarBoards ?? []}
> >
{#key pageKey} {#key pageKey}
<div in:fade={{ duration: 150, delay: 75 }} out:fade={{ duration: 75 }}> <div in:fly={{ y: 6, duration: 150 }}>
{@render children()} {@render children()}
</div> </div>
{/key} {/key}
</MainLayout> </MainLayout>
{:else} {:else}
{#key pageKey} {#key pageKey}
<div in:fade={{ duration: 150, delay: 75 }} out:fade={{ duration: 75 }}> <div in:fly={{ y: 6, duration: 150 }}>
{@render children()} {@render children()}
</div> </div>
{/key} {/key}
+1 -1
View File
@@ -89,7 +89,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
const appHistories: Record<string, { history: { status: string; responseTime: number | null; checkedAt: string }[]; uptimePercent: number }> = {}; const appHistories: Record<string, { history: { status: string; responseTime: number | null; checkedAt: string }[]; uptimePercent: number }> = {};
for (const [appId, data] of historyMap) { for (const [appId, data] of historyMap) {
appHistories[appId] = { appHistories[appId] = {
history: data.history.map((h) => ({ ...h, checkedAt: h.checkedAt.toISOString() })), history: data.history.map((h: { status: string; responseTime: number | null; checkedAt: Date }) => ({ ...h, checkedAt: h.checkedAt.toISOString() })),
uptimePercent: data.uptimePercent uptimePercent: data.uptimePercent
}; };
} }
@@ -318,6 +318,7 @@
<!-- Background Type --> <!-- Background Type -->
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground">{$t('settings.background') ?? 'Background'}</label> <label class="mb-1 block text-sm font-medium text-foreground">{$t('settings.background') ?? 'Background'}</label>
<div class="flex flex-wrap gap-1 rounded-lg border border-border bg-muted/50 p-1"> <div class="flex flex-wrap gap-1 rounded-lg border border-border bg-muted/50 p-1">
{#each ['mesh', 'particles', 'aurora', 'wallpaper', 'none'] as bg (bg)} {#each ['mesh', 'particles', 'aurora', 'wallpaper', 'none'] as bg (bg)}
@@ -333,6 +334,7 @@
<!-- Card Size --> <!-- Card Size -->
<div> <div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-1 block text-sm font-medium text-foreground">{$t('board.card_size') ?? 'Card Size'}</label> <label class="mb-1 block text-sm font-medium text-foreground">{$t('board.card_size') ?? 'Card Size'}</label>
<div class="flex gap-1 rounded-lg border border-border bg-muted/50 p-1"> <div class="flex gap-1 rounded-lg border border-border bg-muted/50 p-1">
{#each ['compact', 'medium', 'large'] as size (size)} {#each ['compact', 'medium', 'large'] as size (size)}
+2 -2
View File
@@ -8,8 +8,8 @@
const { form, errors, enhance, submitting } = superForm(data.form); const { form, errors, enhance, submitting } = superForm(data.form);
const showLocalForm = data.authMode === 'local' || data.authMode === 'both'; const showLocalForm = $derived(data.authMode === 'local' || data.authMode === 'both');
const showOAuthButton = data.authMode === 'oauth' || data.authMode === 'both'; const showOAuthButton = $derived(data.authMode === 'oauth' || data.authMode === 'both');
</script> </script>
<svelte:head> <svelte:head>