️ 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:
Wuesteon 2025-12-15 19:09:01 +01:00
parent b949037fa5
commit 42e5e97390
101 changed files with 1048 additions and 558 deletions

View file

@ -4,10 +4,16 @@
* Authentication is handled entirely by Mana Core Auth (@manacore/shared-auth).
* No Supabase is needed - all data comes from mana-core-auth APIs.
*/
import type { UserData } from '@manacore/shared-auth';
declare global {
namespace App {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Locals {}
interface Locals {
session?: {
access_token: string;
user: UserData;
} | null;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface PageData {}
// interface Error {}

View file

@ -1,34 +1,23 @@
<script lang="ts">
/**
* Icon Component - Re-exports from @manacore/shared-icons
* This wrapper ensures backward compatibility with existing imports
* Icon Component - Wrapper for phosphor-svelte icons
* NOTE: This is a legacy wrapper. Use phosphor-svelte icons directly instead.
* Example: import { House, User } from '@manacore/shared-icons';
*/
import { iconPaths } from '@manacore/shared-icons';
interface Props {
name: keyof typeof iconPaths;
name: string;
size?: number;
class?: string;
color?: string;
}
let { name, size = 24, class: className = '', color }: Props = $props();
const path = $derived(iconPaths[name]);
</script>
{#if path}
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill={color || 'currentColor'}
viewBox="0 0 256 256"
class={className}
aria-hidden="true"
>
{@html path}
</svg>
{:else}
<span class="text-red-500" title="Icon '{name}' not found"></span>
{/if}
<span
class="text-orange-500"
title="Icon component is deprecated. Use direct imports from @manacore/shared-icons instead."
>
{name}
</span>

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<CalendarEvent[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -18,18 +18,18 @@
const MAX_DISPLAY = 5;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await calendarService.getUpcomingEvents(7);
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -88,9 +88,9 @@
{/if}
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if (data || []).length === 0}
<div class="py-6 text-center">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Conversation[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -18,18 +18,18 @@
const MAX_DISPLAY = 5;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await chatService.getRecentConversations(MAX_DISPLAY);
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -69,9 +69,9 @@
</h3>
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
<div class="py-6 text-center">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let timers = $state<Timer[]>([]);
let alarms = $state<Alarm[]>([]);
let stats = $state<ClockStats | null>(null);
@ -18,7 +18,7 @@
let retryCount = $state(0);
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const [timersResult, alarmsResult, statsResult] = await Promise.all([
@ -31,11 +31,11 @@
timers = timersResult.data;
alarms = alarmsResult.data.slice(0, 3);
stats = statsResult.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = timersResult.error || alarmsResult.error || statsResult.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -79,9 +79,9 @@
</h3>
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={3} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if timers.length === 0 && alarms.length === 0}
<div class="py-6 text-center">

View file

@ -10,7 +10,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Contact[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -23,18 +23,18 @@
const contactsUrl = isDev ? APP_URLS.contacts.dev : APP_URLS.contacts.prod;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await contactsService.getFavoriteContacts(MAX_DISPLAY);
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -71,9 +71,9 @@
</h3>
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
<div class="py-6 text-center">

View file

@ -9,22 +9,22 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<CreditBalance | null>(null);
let error = $state<string | null>(null);
let retrying = $state(false);
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
try {
const balance = await creditsService.getBalance();
data = balance;
state = 'success';
loadingState = 'success';
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load credits';
state = 'error';
loadingState = 'error';
} finally {
retrying = false;
}
@ -43,9 +43,9 @@
{$_('dashboard.widgets.credits.title')}
</h3>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={3} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data}
<div class="space-y-3">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let progress = $state<LearningProgress | null>(null);
let decks = $state<Deck[]>([]);
let error = $state<string | null>(null);
@ -17,7 +17,7 @@
let retryCount = $state(0);
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const [progressResult, decksResult] = await Promise.all([
@ -28,11 +28,11 @@
if (progressResult.data && decksResult.data) {
progress = progressResult.data;
decks = decksResult.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = progressResult.error || decksResult.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -55,10 +55,10 @@
);
// Get decks with due cards
const decksWithDue = $derived(decks.filter((d) => d.dueCount > 0).slice(0, 3));
const decksWithDue = $derived(decks.filter((d: Deck) => d.dueCount > 0).slice(0, 3));
// Total due cards
const totalDue = $derived(decks.reduce((sum, d) => sum + d.dueCount, 0));
const totalDue = $derived(decks.reduce((sum: number, d: Deck) => sum + d.dueCount, 0));
</script>
<div>
@ -69,9 +69,9 @@
</h3>
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if !progress || decks.length === 0}
<div class="py-6 text-center">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<GeneratedImage[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -18,18 +18,18 @@
const MAX_DISPLAY = 6;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await pictureService.getRecentGenerations(MAX_DISPLAY);
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -74,9 +74,9 @@
</h3>
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={3} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
<div class="py-6 text-center">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let stats = $state<ReferralStats | null>(null);
let code = $state<ReferralCode | null>(null);
let error = $state<string | null>(null);
@ -17,7 +17,7 @@
let copied = $state(false);
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
try {
@ -27,10 +27,10 @@
]);
stats = statsData;
code = codeData;
state = 'success';
loadingState = 'success';
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load referral data';
state = 'error';
loadingState = 'error';
} finally {
retrying = false;
}
@ -81,9 +81,9 @@
{$_('dashboard.widgets.referral.title')}
</h3>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if stats && code}
<div class="space-y-4">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Task[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -18,18 +18,18 @@
const MAX_DISPLAY = 5;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await todoService.getTodayTasks();
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -74,9 +74,9 @@
{/if}
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if (data || []).length === 0}
<div class="py-6 text-center">

View file

@ -9,7 +9,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Task[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -18,18 +18,18 @@
const MAX_DISPLAY = 5;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await todoService.getUpcomingTasks(7);
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -77,9 +77,9 @@
{/if}
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
<div class="py-6 text-center">

View file

@ -9,22 +9,22 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<CreditTransaction[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
try {
const transactions = await creditsService.getTransactions(5);
data = transactions;
state = 'success';
loadingState = 'success';
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load transactions';
state = 'error';
loadingState = 'error';
} finally {
retrying = false;
}
@ -63,9 +63,9 @@
</a>
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
<p class="py-4 text-center text-sm text-muted-foreground">

View file

@ -10,7 +10,7 @@
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let loadingState = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Favorite | null>(null);
let error = $state<string | null>(null);
let retrying = $state(false);
@ -21,18 +21,18 @@
const zitareUrl = isDev ? APP_URLS.zitare.dev : APP_URLS.zitare.prod;
async function load() {
state = 'loading';
loadingState = 'loading';
retrying = true;
const result = await zitareService.getRandomFavorite();
if (result.data) {
data = result.data;
state = 'success';
loadingState = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
loadingState = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
@ -58,7 +58,7 @@
<span>=<3D></span>
{$_('dashboard.widgets.zitare.title')}
</h3>
{#if state === 'success' && data}
{#if loadingState === 'success' && data}
<button
type="button"
onclick={loadNewQuote}
@ -73,9 +73,9 @@
{/if}
</div>
{#if state === 'loading'}
{#if loadingState === 'loading'}
<WidgetSkeleton lines={3} />
{:else if state === 'error'}
{:else if loadingState === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if !data}
<div class="py-6 text-center">

View file

@ -1,5 +1,15 @@
import type { PageServerLoad } from './$types';
export interface Organization {
id: string;
name: string;
user_role?: string;
total_credits?: number;
used_credits?: number;
team_count?: number;
created_at: string;
}
/**
* Organizations page server load
*
@ -10,6 +20,6 @@ export const load: PageServerLoad = async () => {
// Return empty data - auth is handled client-side
// TODO: Implement client-side data fetching with Mana Core Auth token
return {
organizations: [],
organizations: [] as Organization[],
};
};

View file

@ -1,11 +1,12 @@
<script lang="ts">
import { Card, Button, PageHeader } from '@manacore/shared-ui';
import type { PageData } from './$types';
import type { Organization } from './+page.server';
let { data }: { data: PageData } = $props();
function getAvailableCredits(org: any) {
return org.total_credits - org.used_credits;
function getAvailableCredits(org: Organization) {
return (org.total_credits || 0) - (org.used_credits || 0);
}
function getRoleBadgeColor(role: string) {
@ -77,8 +78,10 @@
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
class="h-full rounded-full bg-primary-600 transition-all"
style="width: {((org.total_credits - org.used_credits) / org.total_credits) *
100}%"
style="width: {org.total_credits
? (((org.total_credits || 0) - (org.used_credits || 0)) / org.total_credits) *
100
: 0}%"
></div>
</div>
</div>

View file

@ -1,5 +1,18 @@
import type { PageServerLoad } from './$types';
export interface Team {
id: string;
name: string;
organization?: {
name: string;
};
user_role?: string;
allocated_credits?: number;
used_credits?: number;
member_count?: number;
created_at: string;
}
/**
* Teams page server load
*
@ -10,6 +23,6 @@ export const load: PageServerLoad = async () => {
// Return empty data - auth is handled client-side
// TODO: Implement client-side data fetching with Mana Core Auth token
return {
teams: [],
teams: [] as Team[],
};
};

View file

@ -1,11 +1,12 @@
<script lang="ts">
import { Card, Button, PageHeader } from '@manacore/shared-ui';
import type { PageData } from './$types';
import type { Team } from './+page.server';
let { data }: { data: PageData } = $props();
function getAvailableCredits(team: any) {
return team.allocated_credits - team.used_credits;
function getAvailableCredits(team: Team) {
return (team.allocated_credits || 0) - (team.used_credits || 0);
}
function getRoleBadgeColor(role: string) {
@ -74,7 +75,9 @@
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
class="h-full rounded-full bg-primary-600 transition-all"
style="width: {(getAvailableCredits(team) / team.allocated_credits) * 100}%"
style="width: {team.allocated_credits
? (getAvailableCredits(team) / team.allocated_credits) * 100
: 0}%"
></div>
</div>
</div>

View file

@ -6,12 +6,12 @@
let { children } = $props();
onMount(async () => {
onMount(() => {
// Initialize theme
const cleanupTheme = theme.initialize();
// Initialize auth
await authStore.initialize();
// Initialize auth (non-blocking)
authStore.initialize();
return () => {
cleanupTheme();

View file

@ -1,14 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { PageData } from './$types';
let { data } = $props();
let { data }: { data: PageData } = $props();
$effect(() => {
if (!data.session) {
goto('/login');
} else {
goto('/dashboard');
}
// Redirect to dashboard if already logged in, otherwise go to login
// Auth is handled client-side via Mana Core Auth
goto('/dashboard');
});
</script>

View file

@ -5,6 +5,10 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
compilerOptions: {
runes: true,
},
kit: {
adapter: adapter({
out: 'build',