{#if showCreateModal} - - e.stopPropagation()}> + + e.key === 'Escape' && closeCreateModal()} + role="dialog" + aria-modal="true" + tabindex="-1" + > + + e.stopPropagation()} onkeydown={() => {}} role="document"> Neue Liste erstellen @@ -283,26 +292,6 @@ margin: 0 auto var(--spacing-xl); } - .header-row { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--spacing-md); - margin-bottom: var(--spacing-lg); - } - - h2 { - font-size: 2rem; - margin: 0 0 var(--spacing-xs) 0; - color: rgb(var(--color-text-primary)); - } - - .subtitle { - font-size: 0.875rem; - color: rgb(var(--color-text-secondary)); - margin: 0; - } - .create-fab { display: flex; align-items: center; @@ -646,10 +635,6 @@ max-width: 100%; } - h2 { - font-size: 1.5rem; - } - .create-fab { width: 2.5rem; height: 2.5rem; diff --git a/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte b/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte index f51473a0f..9177bc26c 100644 --- a/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte +++ b/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte @@ -37,7 +37,7 @@ let listQuotes = $derived( list ? quotesDE - .filter((quote) => list.quoteIds.includes(quote.id)) + .filter((quote) => list!.quoteIds.includes(quote.id)) .map((quote) => ({ ...quote, author: authorsDE.find((a) => a.id === quote.authorId), @@ -126,7 +126,7 @@ if (list) { const count = selectedQuoteIds.size; selectedQuoteIds.forEach((quoteId) => { - listsStore.addQuoteToList(list.id, quoteId); + listsStore.addQuoteToList(list!.id, quoteId); }); toast.success(`${count} ${count === 1 ? 'Zitat' : 'Zitate'} hinzugefügt!`); closeAddQuotesModal(); @@ -359,8 +359,17 @@ {#if showEditModal} - - e.stopPropagation()}> + + e.key === 'Escape' && closeEditModal()} + role="dialog" + aria-modal="true" + tabindex="-1" + > + + e.stopPropagation()} onkeydown={() => {}} role="document"> Liste bearbeiten @@ -423,8 +432,22 @@ {#if showAddQuotesModal} - - e.stopPropagation()}> + + e.key === 'Escape' && closeAddQuotesModal()} + role="dialog" + aria-modal="true" + tabindex="-1" + > + + e.stopPropagation()} + onkeydown={() => {}} + role="document" + > Zitate hinzufügen diff --git a/apps/zitare/apps/web/src/routes/(app)/search/+page.svelte b/apps/zitare/apps/web/src/routes/(app)/search/+page.svelte index f4f22c1cc..887b2a498 100644 --- a/apps/zitare/apps/web/src/routes/(app)/search/+page.svelte +++ b/apps/zitare/apps/web/src/routes/(app)/search/+page.svelte @@ -147,6 +147,7 @@ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> + + doSomething()}>Click me + + + doSomething()} + onkeydown={(e) => e.key === 'Enter' && doSomething()} + role="button" + tabindex="0" +> + Click me + + + + doSomething()}>Click me +``` + +### 2. Non-interactive Element with Interactions + +**Warning:** `a11y_no_static_element_interactions` + +```svelte + +Click me + + + {}} role="button" tabindex="0"> + Click me + + + + + {}}> +``` + +### 3. Buttons Need Labels + +**Warning:** `a11y_consider_explicit_label` + +```svelte + + + ... + + + + + ... + +``` + +### 4. Autofocus Warning + +**Warning:** `a11y_autofocus` + +```svelte + + + +``` + +### 5. Interactive Role Needs Focus + +**Warning:** `a11y_interactive_supports_focus` + +```svelte + +Menu + + + {}}>Menu +``` + +### 6. Nested Interactive Elements + +**Warning:** `node_invalid_placement_ssr` + +```svelte + + + Delete + + + + + + Delete + + + + + Select + Delete + +``` + +### 7. Svelte 5 Reactivity + +**Warning:** `non_reactive_update` + +```svelte + + + + + +``` + +--- + +## Modal Pattern (Common Fix) + +Most modal warnings can be fixed with this pattern: + +```svelte +{#if showModal} + + + (showModal = false)} + onkeydown={(e) => e.key === 'Escape' && (showModal = false)} + role="presentation" + > + + + e.stopPropagation()} + onkeydown={() => {}} + role="dialog" + aria-modal="true" + tabindex="-1" + > + + + +{/if} +``` + +--- + +## Dropdown/Menu Pattern + +```svelte +{#if showDropdown} + + e.stopPropagation()} + onkeydown={() => {}} + role="menu" + tabindex="-1" + > + selectOption('a')}>Option A + selectOption('b')}>Option B + +{/if} +``` + +--- + +## Running Checks Manually + +```bash +# Check a specific app +pnpm --filter @todo/web exec svelte-check --threshold warning + +# Check all staged files (same as pre-commit) +./scripts/svelte-check-staged.sh + +# Quick check without threshold (shows all issues) +pnpm --filter @todo/web exec svelte-check +``` + +--- + +## Bypassing Pre-commit (Emergency Only) + +If you absolutely must commit without checks (e.g., WIP branch): + +```bash +git commit --no-verify -m "WIP: work in progress" +``` + +**Warning:** This bypasses ALL pre-commit hooks. Use sparingly and fix issues before PR. + +--- + +## Files + +| File | Purpose | +|------|---------| +| `.husky/pre-commit` | Runs lint-staged, type-check, and svelte-check | +| `scripts/svelte-check-staged.sh` | Detects affected apps and runs checks | +| `docs/SVELTE_CHECK_ISSUES.md` | This documentation | diff --git a/packages/shared-auth/src/core/jwtUtils.ts b/packages/shared-auth/src/core/jwtUtils.ts index 0eb39c9c9..1b8ce026b 100644 --- a/packages/shared-auth/src/core/jwtUtils.ts +++ b/packages/shared-auth/src/core/jwtUtils.ts @@ -76,6 +76,7 @@ export function getUserFromToken(token: string, storedEmail?: string): UserData return { id: payload.sub, + sub: payload.sub, email: email || 'user@example.com', role: payload.role || 'user', }; diff --git a/packages/shared-auth/src/types/index.ts b/packages/shared-auth/src/types/index.ts index 32d233bb0..25e0c0dcc 100644 --- a/packages/shared-auth/src/types/index.ts +++ b/packages/shared-auth/src/types/index.ts @@ -48,6 +48,7 @@ export interface DecodedToken { */ export interface UserData { id: string; + sub: string; // JWT subject (user ID) email: string; role: string; } diff --git a/packages/shared-splitscreen/src/components/ResizeHandle.svelte b/packages/shared-splitscreen/src/components/ResizeHandle.svelte index 843573e7b..f652bb0b6 100644 --- a/packages/shared-splitscreen/src/components/ResizeHandle.svelte +++ b/packages/shared-splitscreen/src/components/ResizeHandle.svelte @@ -109,12 +109,13 @@ } + import type { Snippet } from 'svelte'; - type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'outline' | 'success'; + type ButtonVariant = + | 'primary' + | 'secondary' + | 'ghost' + | 'danger' + | 'destructive' + | 'outline' + | 'success'; type ButtonSize = 'sm' | 'md' | 'lg' | 'xl'; interface Props { @@ -31,6 +38,7 @@ secondary: 'bg-menu text-theme hover:bg-menu-hover border-theme', ghost: 'bg-transparent text-theme hover:bg-menu-hover border-transparent', danger: 'bg-red-600 text-white hover:bg-red-700 border-transparent', + destructive: 'bg-red-600 text-white hover:bg-red-700 border-transparent', outline: 'bg-transparent text-primary border-primary hover:bg-primary/10', success: 'bg-green-600 text-white hover:bg-green-700 border-transparent', }; diff --git a/packages/shared-ui/src/context-menu/ContextMenu.svelte b/packages/shared-ui/src/context-menu/ContextMenu.svelte index 997a6a1bd..d9fe09ad9 100644 --- a/packages/shared-ui/src/context-menu/ContextMenu.svelte +++ b/packages/shared-ui/src/context-menu/ContextMenu.svelte @@ -86,7 +86,6 @@ {#if visible} - { @@ -104,9 +103,17 @@ e.stopPropagation(); onClose(); }} + onkeydown={(e) => { + if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + }} + role="presentation" + aria-hidden="true" > - - - + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleTriggerClick(e as unknown as MouseEvent); + } + }} + role="button" + tabindex="0" + aria-expanded={visible} +> {@render children()} diff --git a/packages/shared-ui/src/molecules/Input.svelte b/packages/shared-ui/src/molecules/Input.svelte index 001840ab5..7599569eb 100644 --- a/packages/shared-ui/src/molecules/Input.svelte +++ b/packages/shared-ui/src/molecules/Input.svelte @@ -16,6 +16,8 @@ class?: string; id?: string; name?: string; + minlength?: number; + maxlength?: number; } let { @@ -33,6 +35,8 @@ class: className = '', id = `input-${Math.random().toString(36).slice(2, 9)}`, name, + minlength, + maxlength, }: Props = $props(); function handleInput(e: Event) { @@ -65,6 +69,8 @@ {placeholder} {disabled} {required} + {minlength} + {maxlength} autocomplete={autocomplete as HTMLInputAttributes['autocomplete']} oninput={handleInput} onchange={handleChange} diff --git a/packages/shared-ui/src/organisms/network/NetworkControls.svelte b/packages/shared-ui/src/organisms/network/NetworkControls.svelte index 47f0df22f..f6a4c9b71 100644 --- a/packages/shared-ui/src/organisms/network/NetworkControls.svelte +++ b/packages/shared-ui/src/organisms/network/NetworkControls.svelte @@ -56,6 +56,7 @@ let showFilters = $state(false); let showKeyboardHelp = $state(false); let strengthValue = $state(minStrength); + // svelte-ignore non_reactive_update - Element reference doesn't need reactivity let searchInputElement: HTMLInputElement; // Sync searchInput with external searchQuery diff --git a/scripts/svelte-check-staged.sh b/scripts/svelte-check-staged.sh index 9ed1caf78..e748aed44 100755 --- a/scripts/svelte-check-staged.sh +++ b/scripts/svelte-check-staged.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Run svelte-check on web apps that have staged .svelte files # This catches a11y warnings, Svelte 5 issues, and import errors before CI @@ -13,23 +13,33 @@ if [ -z "$STAGED_SVELTE" ]; then fi # Find unique web app directories that have changes -declare -A WEB_APPS +WEB_APPS="" for file in $STAGED_SVELTE; do # Extract the web app path (e.g., apps/todo/apps/web) + app_path="" if [[ $file =~ ^(apps/[^/]+/apps/web)/ ]]; then - WEB_APPS["${BASH_REMATCH[1]}"]=1 + app_path="${BASH_REMATCH[1]}" elif [[ $file =~ ^(games/[^/]+/apps/web)/ ]]; then - WEB_APPS["${BASH_REMATCH[1]}"]=1 + app_path="${BASH_REMATCH[1]}" elif [[ $file =~ ^(packages/[^/]+)/ ]]; then # For shared packages, check all web apps that might use them - # This is a simplified approach - just warn echo "⚠️ Changes in shared package: $file" echo " Consider running: pnpm run build:check to verify all web apps" fi + + # Add to list if not already present + if [ -n "$app_path" ]; then + if [[ ! " $WEB_APPS " =~ " $app_path " ]]; then + WEB_APPS="$WEB_APPS $app_path" + fi + fi done -if [ ${#WEB_APPS[@]} -eq 0 ]; then +# Trim leading space +WEB_APPS=$(echo "$WEB_APPS" | xargs) + +if [ -z "$WEB_APPS" ]; then echo "No web app changes detected" exit 0 fi @@ -37,7 +47,7 @@ fi echo "🔍 Running svelte-check on affected web apps..." FAILED=0 -for app in "${!WEB_APPS[@]}"; do +for app in $WEB_APPS; do if [ -f "$app/package.json" ]; then echo "" echo "━━━ Checking $app ━━━" @@ -46,7 +56,8 @@ for app in "${!WEB_APPS[@]}"; do PKG_NAME=$(node -p "require('./$app/package.json').name" 2>/dev/null || echo "") if [ -n "$PKG_NAME" ]; then - # Run svelte-check with threshold to fail on warnings + # Run svelte-check - fails on both errors AND warnings + # This ensures no a11y issues or Svelte problems slip through if ! pnpm --filter "$PKG_NAME" exec svelte-check --tsconfig ./tsconfig.json --threshold warning 2>&1; then echo "❌ svelte-check failed for $app" FAILED=1