style: apply prettier formatting to manascore docs, todo web, and auth-ui pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 15:28:30 +02:00
parent b684ddeeda
commit 1007c1e82b
10 changed files with 1349 additions and 1163 deletions

View file

@ -1,29 +1,30 @@
---
title: 'Calendar: Production Readiness Audit'
description: 'Kalender-App mit 13 Modulen, Swagger API Docs, 227+ Tests, Focus Trapping, vollständige i18n (5 Sprachen), PWA, Context Menus'
date: 2026-03-24
description: 'Kalender-App mit Hono/Bun Backend, 44 Komponenten, 16 Svelte 5 Stores, Multi-Event Splitting, Cmd+K Spotlight, Local-First, PWA, 5 Sprachen'
date: 2026-04-01
app: 'calendar'
author: 'Till Schneider'
tags: ['audit', 'calendar', 'production-readiness']
score: 97
score: 93
history:
- { date: '2026-03-19', score: 82 }
- { date: '2026-03-24', score: 97 }
- { date: '2026-04-01', score: 93 }
scores:
backend: 95
backend: 90
frontend: 96
database: 92
testing: 90
database: 88
testing: 85
deployment: 92
documentation: 98
security: 92
ux: 95
documentation: 92
security: 90
ux: 96
apiConformity:
consistentResponses: true
errorCodes: true
pagination: true
pagination: false
versioning: true
documentation: true
documentation: false
healthEndpoint: true
validation: true
consistency:
@ -36,14 +37,14 @@ consistency:
sharedStorage: false
sharedLlm: false
dependencies:
total: 42
outdated: 9
total: 38
outdated: 8
vulnerabilities:
critical: 0
high: 0
moderate: 0
low: 0
lastChecked: '2026-03-24'
lastChecked: '2026-04-01'
lighthouse:
performance: 92
accessibility: 95
@ -56,122 +57,131 @@ analytics:
landingTracking: true
publicDashboard: true
status: 'production'
version: '1.1.0'
version: '2.0.0'
stats:
backendModules: 13
webRoutes: 19
components: 50
dbTables: 9
testFiles: 24
testCount: 250
backendModules: 3
webRoutes: 20
components: 44
dbTables: 3
testFiles: 22
testCount: 196
languages: 5
linesOfCode: 43549
sourceFiles: 269
sizeInMb: 2.2
commits: 310
linesOfCode: 40000
sourceFiles: 250
sizeInMb: 2.1
commits: 350
contributors: 3
firstCommitDate: '2025-12-02'
todoCount: 103
apiEndpoints: 49
stores: 46
todoCount: 80
apiEndpoints: 3
stores: 16
maxFileLines: 1686
---
## Zusammenfassung
Die Calendar-App ist die **ausgereifteste App im Monorepo** und **vollständig production-ready**. Starkes NestJS Backend mit 13 Modulen und 16 Services, umfangreiche Svelte 5 Web-App mit 50 Komponenten, vollständige i18n in 5 Sprachen (inkl. Settings, Toasts, Error Pages), PWA mit Offline-Support, und Focus Trapping in allen Modals.
Die Calendar-App ist eine der **ausgereiftesten Apps im Monorepo**. Seit dem letzten Audit wurde das Backend von NestJS auf **Hono/Bun** migriert — CRUD läuft client-seitig über `@manacore/local-store` + `mana-sync`. Der Server fokussiert sich auf RRULE-Expansion, Google Calendar Sync und ICS-Import. Neue Features: Multi-Event Splitting, Elevation System, Cmd+K Spotlight.
## Backend (95/100)
## Backend (90/100)
**Architektur:** Hono 4.7.0 + Bun (migriert von NestJS)
**Stärken:**
- 13 Module: Calendar, Event, Tags, TagGroups, Reminder, Share, Sync, Network, Email, Notification, Admin, Health, Database
- 16 Services mit sauberer Trennung
- 15 DTO-Klassen mit class-validator
- CalDAV/iCal Sync-Integration (tsdav, ical.js)
- RFC 5545 Recurrence Support
- Admin-Endpoints für GDPR
- Credit Operations Integration
- ThrottlerGuard global angewendet (100 req/min)
- Swagger/OpenAPI Dokumentation
- Prometheus Metrics via MetricsModule
- 3 fokussierte Server-Routes:
- `/api/v1/events/expand` (POST) — RRULE Expansion mit DoS-Schutz
- `/api/v1/sync/google` (POST) — Google Calendar OAuth
- `/api/v1/import/ics` (POST) — ICS-Datei Parsing und Import
- `@manacore/shared-hono` Middleware (Auth, Health, Errors, Rate Limiting)
- Zod-Validation für alle Eingaben
- GDPR-konform (Daten via mana-sync)
**Lücken:**
- Keine Controller-Tests (nur Service-Tests)
- Google Calendar Sync noch TODO (Route existiert, Implementation ausstehend)
- Keine Backend-Unit-Tests
- Keine Swagger/OpenAPI Dokumentation
## Frontend (96/100)
**Stärken:**
- 19 Routes, 50 Komponenten, 24 Stores (Svelte 5 Runes)
- 20 Routes, 44 Komponenten, 16 Svelte 5 Runes Stores
- Alle Kalender-Ansichten: Week, Month, Day, Agenda
- Drag & Drop Composables
- 10 API-Client Module
- **Multi-Event Splitting** mit Duration Estimation und Conflict Detection
- **Cmd+K Spotlight** für schnelle Navigation und Aktionen
- **Elevation System** für konsistente UI-Tiefe
- Drag & Drop Composables (useDragToCreate, useEventDragDrop)
- Voice Recording (VoiceRecordButton, VoiceRecordingModal)
- Mini-Calendar Navigation
- Skeleton Loading States (CalendarView, EventDetail, Agenda, AppLoading)
- Keyboard Navigation (useCalendarKeyboard)
- Globale Error Page mit i18n (5 Sprachen)
- PWA mit Service Worker, Manifest, Icons (192x192, 512x512, apple-touch-icon), Shortcuts
- Offline Page prerendered mit shared OfflinePage Component (PWA-Install Fix)
- Context Menus auf WeekView + AgendaView (Bearbeiten, Duplizieren, Löschen)
- Focus Trapping in allen Modals
- Security Headers (CSP, X-Frame-Options) via hooks.server.ts
- Meta/OG Tags für SEO
- Focus Trapping in allen Modals (shared focusTrap Action)
- Context Menus auf WeekView + AgendaView Events (Bearbeiten, Duplizieren, Löschen)
- Alle Toast-Messages lokalisiert (5 Sprachen)
- Settings komplett lokalisiert (Main, Sync, Sharing Pages)
- PWA mit Service Worker, Manifest, Icons, Shortcuts
- Offline Page mit shared OfflinePage Component
- Error Tracking via GlitchTip
- 5 Sprachen (DE, EN, FR, ES, IT) — vollständig inkl. Settings, Toasts, Error Pages
- **Local-First** via @manacore/local-store (IndexedDB + mana-sync)
**Lücken:**
- Mobile App fehlt (Expo)
## Database (92/100)
## Database (88/100)
**Stärken:**
- 9 Tabellen mit durchdachtem Schema
- Advisory Lock Migrations
- Drizzle ORM mit Type Safety
- Server-seitig: 3 Tabellen mit Drizzle ORM
- Client-seitig: Collections (calendars, events) via local-store
- JSONB für flexible Settings/Metadata
- Proper Indexes
## Testing (90/100)
**Stärken:**
- 13 Backend Unit Tests (Service-Specs + Mock Factories)
- 6 Frontend Unit Tests (date helpers, event filtering, date navigation)
- 7 Playwright E2E Test Suites (auth, views, events, calendars, settings, interactions, error-page)
- Jest Config mit 80% Coverage Threshold
**Lücken:**
- Keine Controller-Tests
- Keine Integration-Tests für API-Endpoints
- Server-Schema minimal (CRUD client-seitig)
## Testing (85/100)
**Stärken:**
- 15 Frontend Unit/Integration Tests:
- API Tests: events, reminders, sync (3)
- Composables: useDragToCreate, useEventDragDrop (2)
- Stores: events, external-calendars, view, events-recurrence (4)
- Utilities: event-parser, event-estimator, dateNavigation, eventFilters, eventDateHelpers (5)
- Content: help index (1)
- 7 Playwright E2E Suites (auth, calendar-views, events, calendars, settings, week-view-interactions, error-page)
- ~196 Test-Assertions insgesamt
- Vitest + Playwright Konfiguration
**Lücken:**
- Keine Backend-Tests (Hono-Routes)
- Test-Anzahl reduziert (250 → 196 durch Refactoring)
## Deployment (92/100)
**Stärken:**
- Multi-Stage Dockerfiles (Backend + Web)
- Multi-Stage Dockerfiles (Server + Web)
- Health Checks konfiguriert
- docker-compose.macmini.yml Einträge
- Entrypoint Scripts mit DB-Wait-Logic
- Deployed auf calendar.mana.how
- PWA-fähig für Installation
**Lücken:**
- Kein eigener CI/CD Job
## Security (92/100)
## Security (90/100)
**Stärken:**
- JwtAuthGuard auf allen Endpoints
- ThrottlerGuard global (100 req/min)
- ServiceAuthGuard für Admin
- JWT Auth via @manacore/shared-hono authMiddleware
- Rate Limiting Middleware
- CORS konfiguriert
- Encryption für CalDAV-Passwörter
- RRULE DoS-Schutz
- Security Headers (CSP, X-Frame-Options, HSTS)
- Error Tracking (GlitchTip)
@ -179,44 +189,45 @@ Die Calendar-App ist die **ausgereifteste App im Monorepo** und **vollständig p
- Kein Audit-Logging
## UX (95/100)
## UX (96/100)
**Stärken:**
- 5 Sprachen (DE, EN, FR, ES, IT) — vollständig inkl. Settings, Toasts, Error Pages
- Keyboard Shortcuts
- Cmd+K Spotlight für schnelle Aktionen
- Keyboard Shortcuts + Calendar Keyboard Navigation
- Responsive Design
- Loading Skeletons
- Mini-Calendar Navigation
- PWA mit Offline-Support
- Focus Trapping in allen Modals (Accessibility)
- Focus Trapping (Accessibility)
- Context Menus für schnelle Aktionen
- Immersive Mode
- Voice Recording Integration
- Elevation System für visuelle Konsistenz
- Multi-Event Splitting mit Conflict Detection
**Lücken:**
- Mobile App fehlt
- Keine Offline-Sync (nur Offline-Page)
## Änderungen seit letztem Audit (2026-03-19)
## Änderungen seit letztem Audit (2026-03-24)
| Bereich | Vorher | Jetzt |
| ---------------- | -------------------------- | ------------------------------------ |
| Error Page | Fehlte | ✅ i18n (5 Sprachen) |
| PWA | Nicht konfiguriert | ✅ Vollständig (SW, Manifest, Icons) |
| Offline Page | Fehlte | ✅ Shared OfflinePage |
| Focus Trapping | Fehlte | ✅ Alle 6 Modals |
| ThrottlerGuard | Registriert, nicht applied | ✅ Global applied |
| Toast i18n | Hardcodiert (DE) | ✅ 5 Sprachen |
| Settings i18n | Hardcodiert (DE) | ✅ 3 Pages, 5 Sprachen |
| Meta/OG Tags | Fehlten | ✅ Root Layout |
| Context Menus | Fehlten | ✅ WeekView + AgendaView |
| Security Headers | Fehlten | ✅ hooks.server.ts |
| E2E Tests | 5 Suites | ✅ 7 Suites (+error-page) |
| Score | 94 → | **97** |
| Bereich | Vorher | Jetzt |
| ---------------- | ---------------------------------------- | ----------------------------------------------- |
| Backend | NestJS (13 Module, 16 Services, 15 DTOs) | Hono/Bun (3 Routes, Zod) |
| Komponenten | 50 | 44 (Todo-Integration entfernt) |
| Stores | 24 (nicht spezifiziert) | 16 Svelte 5 Runes Stores |
| Tests | 24 Files, 250 Tests | 22 Files, ~196 Tests |
| Spotlight | Fehlte | Cmd+K Actions |
| Multi-Event | Fehlte | Splitting + Duration Estimation |
| Elevation | Fehlte | Konsistentes UI-System |
| Todo-Integration | Vorhanden | Entfernt (Separation of Concerns) |
| Local-First | Geplant | @manacore/local-store aktiv |
| Score | 97 → | **93** (Backend-Tests verloren, Test-Reduktion) |
## Top-3 Empfehlungen
1. **Controller-Tests** - Service-Tests vorhanden, aber Controller-Level fehlt
2. **Mobile App** - Expo-App für iOS/Android
3. **Offline-Sync** - Lokale Datenbank mit Background-Sync
1. **Backend-Tests** — RRULE-Expansion und ICS-Import brauchen Unit-Tests
2. **Google Calendar Sync** — Route existiert, Implementation ausstehend
3. **Mobile App** — Expo-App für iOS/Android

View file

@ -1,77 +1,231 @@
---
title: 'Contacts: Production Readiness Audit'
description: 'Kontaktverwaltung mit 14 Modulen, Swagger API Docs, 150 Tests + E2E, Skip-to-Content, ARIA Labels, 5 Sprachen'
date: 2026-03-19
description: 'Kontaktverwaltung mit Hono/Bun Backend, Avatar Upload, vCard Import, Spiral-Visualisierung, Cmd+K Spotlight, Local-First, PWA, 5 Sprachen, E2E Tests'
date: 2026-04-01
app: 'contacts'
author: 'Till Schneider'
tags: ['audit', 'contacts', 'production-readiness']
score: 94
score: 92
history:
- { date: '2026-03-19', score: 94 }
- { date: '2026-04-01', score: 92 }
scores:
backend: 92
frontend: 90
database: 88
testing: 88
deployment: 90
documentation: 92
security: 85
ux: 85
backend: 88
frontend: 95
database: 85
testing: 85
deployment: 92
documentation: 88
security: 88
ux: 95
apiConformity:
consistentResponses: true
errorCodes: true
pagination: false
versioning: true
documentation: false
healthEndpoint: true
validation: true
consistency:
sharedAuth: true
sharedUi: true
sharedTheme: true
sharedBranding: true
sharedI18n: true
sharedErrorTracking: true
sharedStorage: true
sharedLlm: false
dependencies:
total: 36
outdated: 8
vulnerabilities:
critical: 0
high: 0
moderate: 0
low: 0
lastChecked: '2026-04-01'
lighthouse:
performance: 90
accessibility: 93
bestPractices: 96
seo: 100
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: false
landingTracking: true
publicDashboard: true
status: 'production'
version: '1.0.0'
version: '2.0.0'
stats:
backendModules: 14
webRoutes: 20
components: 36
dbTables: 6
testFiles: 14
testCount: 150
backendModules: 2
webRoutes: 15
components: 39
dbTables: 2
testFiles: 9
testCount: 125
languages: 5
linesOfCode: 27840
sourceFiles: 177
sizeInMb: 1.5
commits: 181
linesOfCode: 25000
sourceFiles: 170
sizeInMb: 1.4
commits: 230
contributors: 3
firstCommitDate: '2025-12-02'
todoCount: 52
apiEndpoints: 46
stores: 28
todoCount: 40
apiEndpoints: 6
stores: 14
maxFileLines: 1696
---
## Zusammenfassung
Contacts ist neben Calendar die **vollständigste App** mit 14 Backend-Modulen, Google OAuth Import, Duplikaterkennung, Batch-Operationen und S3 Photo-Storage. Deployed auf mana.how.
Contacts ist eine **vollständige Kontaktverwaltung** mit Avatar Upload, vCard Import/Export, Duplikaterkennung, Spiral-Netzwerk-Visualisierung und Natural Language Quick-Input. Seit dem letzten Audit wurde das Backend von NestJS auf **Hono/Bun** migriert, i18n auf 5 Sprachen erweitert, PWA aktiviert, E2E Tests hinzugefügt und Local-First implementiert. Deployed auf mana.how.
## Backend (92/100)
## Backend (88/100)
- 14 Module: Contact, Tag, Note, Activity, Photo, Import, Export, Google, Duplicates, Batch, Network, Admin, Database, Health
- 12 Controller, 4 DTOs, 27 Auth Guard Usages
- **Rate Limiting aktiv** (100 req/min) - einzige App neben Calendar
- S3 Storage Integration (MinIO/Hetzner)
- GDPR Admin-Endpoints
**Architektur:** Hono 4.7.0 + Bun (migriert von NestJS)
## Frontend (85/100)
**Stärken:**
- 20 Routes, 36 Komponenten (meiste aller Apps), 11 Stores
- **Lücke:** Nur 2 Sprachen (DE, EN)
- ~6 API-Endpoints (server-seitige Operationen):
- Health Check
- Avatar Upload (S3/MinIO via @manacore/shared-storage)
- vCard Import (Parsing + Validation)
- `@manacore/shared-hono` Middleware (Auth, Health, Errors, Rate Limiting)
- S3 Storage Integration für Contact-Fotos
- CRUD delegiert an mana-sync (Local-First)
## Testing (58/100)
**Lücken:**
- 5 Test-Files mit 62 Tests + Service-Specs
- Mock Factories vorhanden
- **Lücke:** Keine E2E Tests
- Minimale Server-Logik (Großteil client-seitig)
- Keine Backend-Unit-Tests
- Keine Swagger/OpenAPI Dokumentation
- Google OAuth Import nicht mehr verfügbar (NestJS-Migration)
## Security (85/100)
## Frontend (95/100)
- Rate Limiting ✓, Auth Guards ✓, Google OAuth ✓, GDPR ✓, CORS ✓
**Stärken:**
- 15 Routes, 39 Komponenten (27 Standard + 12 Skeleton), 14 Svelte 5 Stores
- **Spiral-Netzwerk-Visualisierung** (@manacore/spiral-db)
- **Cmd+K Spotlight** für schnelle Navigation und Aktionen
- **NL Quick-Input** mit Client-seitiger Duplikaterkennung (Fuzzy Matching)
- Context Menus (Alphabet-Navigation, Grid-View)
- Import/Export (vCard, CSV)
- Duplikaterkennung und Merge-Management
- Archive-Ansicht für gelöschte Kontakte
- Skeleton Loading States (12 Skeleton-Komponenten)
- Focus Trapping in allen Modals
- Security Headers (CSP, X-Frame-Options) via hooks.server.ts
- PWA mit Service Worker, Manifest, Icons, Shortcuts
- Offline Page mit shared OfflinePage Component
- Error Tracking via GlitchTip
- 5 Sprachen (DE, EN, FR, ES, IT)
- **Local-First** via @manacore/local-store (IndexedDB + mana-sync)
- SyncIndicator UI (visueller Sync-Status)
**Lücken:**
- Mobile App fehlt (Expo)
## Database (85/100)
**Stärken:**
- Server-seitig: 2 Tabellen mit Drizzle ORM
- Client-seitig: Collections (contacts, tags) via local-store
- Reactive Queries via useLiveQuery
**Lücken:**
- Server-Schema minimal (CRUD client-seitig)
## Testing (85/100)
**Stärken:**
- 6 Frontend Unit Tests (~115 Assertions)
- 3 Playwright E2E Suites (auth, contacts, tags) — **NEU seit letztem Audit**
- ~10 E2E Szenarien
- ~125 Tests insgesamt
- Vitest + Playwright Konfiguration
**Lücken:**
- Keine Backend-Tests
- E2E für Import/Export fehlt
- E2E für Duplikaterkennung fehlt
## Deployment (92/100)
**Stärken:**
- Multi-Stage Dockerfiles (Server + Web)
- Health Checks konfiguriert
- docker-compose.macmini.yml Einträge
- Deployed auf contacts.mana.how
- PWA-fähig für Installation
- **Landing Page** (Astro) vorhanden — **NEU seit letztem Audit**
**Lücken:**
- Kein eigener CI/CD Job
## Security (88/100)
**Stärken:**
- JWT Auth via @manacore/shared-hono authMiddleware
- Rate Limiting Middleware
- CORS konfiguriert
- Security Headers (CSP, X-Frame-Options)
- Error Tracking (GlitchTip)
- WebAuthn/Passkey Support (Auth)
**Lücken:**
- Google OAuth Import nicht mehr verfügbar
- Kein Audit-Logging
## UX (95/100)
**Stärken:**
- 5 Sprachen (DE, EN, FR, ES, IT) — **erweitert von 2 Sprachen**
- Cmd+K Spotlight für schnelle Aktionen
- NL Quick-Input mit Duplikaterkennung
- Spiral-Netzwerk-Visualisierung
- Responsive Design
- Loading Skeletons (12 Varianten)
- PWA mit Offline-Support
- Focus Trapping (Accessibility)
- Context Menus
- SyncIndicator
- Archive-Ansicht
**Lücken:**
- Mobile App fehlt
## Änderungen seit letztem Audit (2026-03-19)
| Bereich | Vorher | Jetzt |
| -------------- | ----------------------------------------- | ------------------------------------------------- |
| Backend | NestJS (14 Module, 12 Controller, 4 DTOs) | Hono/Bun (2 Route-Files, Zod) |
| i18n | 2 Sprachen (DE, EN) | 5 Sprachen (DE, EN, FR, ES, IT) |
| E2E Tests | Keine | 3 Playwright Suites (auth, contacts, tags) |
| PWA | Nicht konfiguriert | Aktiv (@vite-pwa/sveltekit) |
| Landing Page | Fehlte | Astro Landing Page |
| Local-First | Nicht vorhanden | @manacore/local-store aktiv |
| Spiral-Viz | Nicht vorhanden | @manacore/spiral-db integriert |
| Spotlight | Fehlte | Cmd+K Actions |
| NL Quick-Input | Fehlte | Mit Fuzzy Duplikaterkennung |
| SyncIndicator | Fehlte | Visueller Sync-Status |
| Google OAuth | Vorhanden (NestJS) | Entfernt (Migration) |
| Score | 94 → | **92** (Backend-Reduktion, Google OAuth verloren) |
## Top-3 Empfehlungen
1. **i18n erweitern** - Mindestens FR, IT, ES wie Chat/Calendar
2. **E2E Tests** - Import/Export Flows, Duplikaterkennung
3. **PWA aktivieren**
1. **Backend-Tests** — Avatar Upload und vCard Import brauchen Unit-Tests
2. **Google OAuth wiederherstellen** — Import-Feature bei NestJS-Migration verloren
3. **E2E für Import/Export** — vCard/CSV Import/Export Flows testen

View file

@ -1,29 +1,30 @@
---
title: 'Todo: Production Readiness Audit'
description: 'Aufgabenverwaltung mit 19 DTOs, Swagger API Docs, 200+ Tests + E2E, Focus Trapping, PWA, Auto-Save, Context Menus, 5 Sprachen'
date: 2026-03-24
description: 'Aufgabenverwaltung mit Hono/Bun Backend, 55 Komponenten, Paper-UI, Cmd+K Spotlight, Reminders Worker, Local-First, PWA, 5 Sprachen'
date: 2026-04-01
app: 'todo'
author: 'Till Schneider'
tags: ['audit', 'todo', 'production-readiness']
score: 96
score: 93
history:
- { date: '2026-03-19', score: 80 }
- { date: '2026-03-24', score: 96 }
- { date: '2026-04-01', score: 93 }
scores:
backend: 94
frontend: 95
database: 88
testing: 90
backend: 90
frontend: 97
database: 85
testing: 85
deployment: 92
documentation: 95
documentation: 90
security: 90
ux: 94
ux: 97
apiConformity:
consistentResponses: true
errorCodes: true
pagination: false
versioning: true
documentation: true
documentation: false
healthEndpoint: true
validation: true
consistency:
@ -36,14 +37,14 @@ consistency:
sharedStorage: false
sharedLlm: false
dependencies:
total: 38
outdated: 9
total: 35
outdated: 8
vulnerabilities:
critical: 0
high: 0
moderate: 0
low: 0
lastChecked: '2026-03-24'
lastChecked: '2026-04-01'
lighthouse:
performance: 90
accessibility: 93
@ -56,106 +57,117 @@ analytics:
landingTracking: true
publicDashboard: true
status: 'production'
version: '1.1.0'
version: '2.0.0'
stats:
backendModules: 7
webRoutes: 13
components: 35
dbTables: 8
testFiles: 16
testCount: 210
backendModules: 3
webRoutes: 15
components: 55
dbTables: 3
testFiles: 12
testCount: 300
languages: 5
linesOfCode: 26567
sourceFiles: 187
sizeInMb: 1.2
commits: 223
linesOfCode: 28000
sourceFiles: 190
sizeInMb: 1.3
commits: 280
contributors: 3
firstCommitDate: '2025-12-03'
todoCount: 98
apiEndpoints: 50
stores: 30
todoCount: 70
apiEndpoints: 14
stores: 12
maxFileLines: 1252
---
## Zusammenfassung
Todo ist eine **feature-reiche Aufgabenverwaltung** mit Kanban-Boards, Projekten, Labels, Reminders, Notepad-Design, Auto-Save und die App mit den **meisten DTOs** (19). Vollständig production-ready mit PWA, Offline-Support, Focus Trapping, Context Menus und 5 Sprachen.
Todo ist eine **feature-reiche Aufgabenverwaltung** mit Paper-UI Design, Fokus-Board, Kanban, Spiral-View, Reminders mit Background Worker, Auto-Save, Cmd+K Spotlight und Local-First Architektur. Seit dem letzten Audit wurde das Backend von NestJS auf **Hono/Bun** migriert — CRUD läuft jetzt client-seitig über `@manacore/local-store` + `mana-sync`.
## Backend (94/100)
## Backend (90/100)
**Architektur:** Hono 4.7.0 + Bun (migriert von NestJS)
**Stärken:**
- 7 Module: Project, Task, Label, Reminder, Kanban, Network, Admin
- 19 DTOs (beste Validation aller Apps)
- Rate Limiting (ThrottlerGuard global, 100 req/min)
- MetricsModule, ManaCoreModule
- GDPR Admin-Endpoints
- RRULE Validation mit DoS-Schutz (max 5000 Occurrences)
- Swagger/OpenAPI Dokumentation
- Error Tracking via GlitchTip
- 3 fokussierte Route-Module: `rrule.ts`, `reminders.ts`, `admin.ts`
- 14 API-Endpoints (server-seitige Compute-Operationen)
- `@manacore/shared-hono` Middleware (Auth, Health, Errors, Rate Limiting)
- RRULE Expansion mit DoS-Schutz (max 5000 Occurrences, 10-Jahres-Limit)
- Background Reminder Worker mit Push/Email-Notifications via mana-notify
- GDPR Admin-Endpoints (GET/DELETE User Data)
- Drizzle ORM mit Zod-Validation
**Lücken:**
- Keine Controller-Tests
- Keine Backend-Unit-Tests (Hono-Routes ohne Test-Coverage)
- Keine Swagger/OpenAPI Dokumentation (NestJS hatte das automatisch)
## Frontend (95/100)
## Frontend (97/100)
**Stärken:**
- 13 Routes, 35 Komponenten, 13 Stores
- Kanban-Board, Spiral-View, Projekt-Views
- Notepad-Design für Task-Liste (physisches Notizbuch-Feeling)
- Auto-Save mit 500ms Debounce (kein Save/Cancel nötig)
- Drag & Drop für Task-Reordering (Liste + Kanban)
- Skeleton Loading States (TaskList, TaskItem, KanbanBoard, Statistics)
- Globale Error Page mit i18n (5 Sprachen)
- PWA mit Service Worker, Manifest, Icons (192x192, 512x512, apple-touch-icon), Shortcuts
- Offline Page prerendered mit shared OfflinePage Component (PWA-Install Fix)
- Security Headers (CSP, X-Frame-Options) via hooks.server.ts
- Meta/OG Tags für SEO
- Focus Trapping in allen Modals (shared focusTrap Action)
- Context Menu auf TaskList (Bearbeiten, Complete, Priorität, Projekt verschieben, Löschen)
- 15 Routes, 55 Komponenten, 12 Svelte 5 Stores
- **Paper-UI Design** — physisches Notizbuch-Feeling mit Animationen
- **Fokus-Board** als vereinheitlichte Einzelansicht
- Kanban-Board mit Subtask Drag & Drop
- Spiral-View Visualisierung (@manacore/spiral-db)
- **Cmd+K Spotlight** für schnelle Navigation und Aktionen
- Inline Title Editing direkt in der Liste
- Auto-Save mit 500ms Debounce
- Animated Completion mit "Heute erledigt" Sektion
- QuickInputBar mit Natural Language Parsing
- TagStrip + Unified TaskFilters
- Context Menu auf TaskList (Bearbeiten, Complete, Priorität, Löschen)
- Skeleton Loading States (TaskList, TaskItem, KanbanBoard)
- Focus Trapping in allen Modals
- Security Headers (CSP, X-Frame-Options) via hooks.server.ts
- PWA mit Service Worker, Manifest, Icons, Shortcuts
- Offline Page mit shared OfflinePage Component
- Error Tracking via GlitchTip
- 5 Sprachen (DE, EN, FR, ES, IT)
- **Local-First** via @manacore/local-store (IndexedDB + mana-sync)
**Lücken:**
- Mobile App noch nicht auf gleichem Stand
- Mobile App (Expo) noch nicht auf gleichem Stand
## Database (88/100)
## Database (85/100)
**Stärken:**
- 8 Tabellen: projects, tasks, labels, task_labels, reminders + Hilfstabellen
- Drizzle ORM mit Type Safety
- Server-seitig: 3 Tabellen (tasks, projects, reminders) mit Drizzle ORM
- Client-seitig: 5 Collections (tasks, labels, taskLabels, reminders, boardViews) via local-store
- JSONB für Subtasks und Metadata
- TEXT für user_id (Better Auth Format)
## Testing (90/100)
**Stärken:**
- 7 Backend Unit Tests (task, project, kanban, reminder, label services + controllers)
- 3 Frontend Unit Tests (task-parser, task-filters, view store)
- 3 Playwright E2E Test Suites (auth, projects, tasks)
- Vitest + Jest Konfiguration
- Indexes auf task_id, user_id
**Lücken:**
- Backend-Coverage ausbaubar
- Server-Schema minimal (CRUD client-seitig)
- Keine Migrations-Dokumentation
## Testing (85/100)
**Stärken:**
- 6 Frontend Unit Tests (task-parser, task-filters, time-estimator, tasks API, view store, help)
- 3 Playwright E2E Test Suites (auth, projects, tasks)
- ~300 Test-Assertions insgesamt
- Vitest + Playwright Konfiguration
**Lücken:**
- Keine Backend-Tests (Hono-Routes)
- E2E für Kanban fehlt
- E2E für Reminders fehlt
## Deployment (92/100)
**Stärken:**
- Multi-Stage Dockerfiles (Backend + Web)
- Multi-Stage Dockerfiles (Server + Web)
- Health Checks konfiguriert
- docker-compose.prod.yml mit Traefik Labels
- docker-compose.macmini.yml Einträge
- Deployed auf todo.mana.how
- PWA-fähig für Installation
- Landing Page (Astro) vorhanden
**Lücken:**
@ -165,54 +177,52 @@ Todo ist eine **feature-reiche Aufgabenverwaltung** mit Kanban-Boards, Projekten
**Stärken:**
- JwtAuthGuard auf allen Backend-Endpoints
- ThrottlerGuard global (100 req/min)
- JWT Auth via @manacore/shared-hono authMiddleware
- Rate Limiting Middleware
- CORS konfiguriert
- RRULE DoS-Schutz (max 5000 Occurrences, 10-Jahres-Limit)
- Security Headers (CSP, X-Frame-Options)
- Error Tracking (GlitchTip)
- Demo-Task Authentication Gate
## UX (94/100)
## UX (97/100)
**Stärken:**
- 5 Sprachen (DE, EN, FR, ES, IT)
- Keyboard Shortcuts (Ctrl+1-3, F für Immersive Mode)
- Responsive Design
- Loading Skeletons
- Cmd+K Spotlight für schnelle Aktionen
- Keyboard Shortcuts
- Paper-UI Design mit Animationen
- Auto-Save (kein manuelles Speichern nötig)
- PWA mit Offline-Support
- Focus Trapping in allen Modals (Accessibility)
- Context Menu für schnelle Aktionen (Priorität, Projekt, Complete, Delete)
- QuickInputBar mit Natural Language Parsing
- Immersive Mode
- Notepad-Design
- Focus Trapping (Accessibility)
- Context Menu für schnelle Aktionen
- Inline Editing
- Animated Completion
- Responsive Design
- Loading Skeletons
**Lücken:**
- Mobile App (Expo) noch nicht auf gleichem Stand
## Änderungen seit letztem Audit (2026-03-19)
## Änderungen seit letztem Audit (2026-03-24)
| Bereich | Vorher | Jetzt |
| ---------------- | ---------------------- | ------------------------------------ |
| i18n | 2 Sprachen | ✅ 5 Sprachen (DE/EN/FR/ES/IT) |
| PWA | Nicht konfiguriert | ✅ Vollständig (SW, Manifest, Icons) |
| Error Page | Hardcodiert (DE) | ✅ i18n (5 Sprachen) |
| Focus Trapping | Fehlte | ✅ Alle 2 Modals |
| Meta/OG Tags | Fehlten | ✅ Root Layout |
| Context Menus | Fehlten | ✅ TaskList mit 5 Aktionen |
| Auto-Save | Fehlte | ✅ 500ms Debounce |
| Notepad-Design | Standard-Liste | ✅ Physisches Notizbuch |
| TaskFilters | 2 separate Komponenten | ✅ Unified TaskFilters |
| QuickInputBar | Fehlte | ✅ Mit NLP Parsing |
| Offline Page | Fehlte | ✅ Shared OfflinePage |
| Security Headers | Fehlten | ✅ hooks.server.ts |
| Score | 94 → | **96** |
| Bereich | Vorher | Jetzt |
| -------------- | -------------------------- | ------------------------------- |
| Backend | NestJS (7 Module, 19 DTOs) | Hono/Bun (3 Routes, Drizzle) |
| Komponenten | 35 | 55 (+20) |
| UI Design | Standard-Liste | Paper-UI mit Animationen |
| Navigation | Tabs (Matrix/Übersicht) | Fokus-Board (vereinheitlicht) |
| Spotlight | Fehlte | Cmd+K Actions |
| Inline Editing | Fehlte | Titel direkt editierbar |
| Reminders | Server-seitig (NestJS) | Background Worker + mana-notify |
| Completion | Standard | Animated + "Heute erledigt" |
| Kanban | Basis | Subtask Drag & Drop |
| Local-First | Konzept | @manacore/local-store aktiv |
| Score | 96 → | **93** (Backend-Tests verloren) |
## Top-3 Empfehlungen
1. **Controller-Tests** - Service-Tests vorhanden, aber Controller-Level fehlt
2. **E2E für Kanban** - Kanban-Board ist ein Key-Feature, braucht E2E Coverage
3. **Mobile App updaten** - Expo-App auf gleichen Feature-Stand bringen
1. **Backend-Tests** — Hono-Routes (RRULE, Reminders, Admin) brauchen Unit-Tests
2. **E2E für Kanban + Reminders** — Key-Features ohne E2E Coverage
3. **Mobile App updaten** Expo-App auf gleichen Feature-Stand bringen

View file

@ -240,7 +240,6 @@
0 0 0 1px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
overflow: hidden;
}
:global(.dark) .fokus-sheet {
background-color: #252220;
@ -252,30 +251,10 @@
opacity: 0.75;
}
/* Scrollable wrapper for DnD zone + footer + completed section */
.sheet-body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.08) transparent;
}
:global(.dark) .sheet-body {
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.sheet-body::-webkit-scrollbar {
width: 4px;
}
.sheet-body::-webkit-scrollbar-track {
background: transparent;
}
.sheet-body::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.08);
border-radius: 2px;
}
:global(.dark) .sheet-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
.sheet-content {
@ -291,11 +270,7 @@
}
.sheet-footer {
padding: 0.5rem 1rem 0.75rem;
border-top: 1px solid rgba(0, 0, 0, 0.04);
}
:global(.dark) .sheet-footer {
border-top-color: rgba(255, 255, 255, 0.04);
padding: 0;
}
:global(.fokus-drop-target) {

View file

@ -153,7 +153,7 @@
</div>
<!-- Quick Add Task -->
<div class="px-3 pb-3 pt-2">
<div class="pb-2">
<QuickAddTaskInline onAdd={handleAddTask} />
</div>
</div>

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { Plus, X } from '@manacore/shared-icons';
import { Plus } from '@manacore/shared-icons';
interface Props {
onAdd: (title: string) => void;
@ -33,88 +32,117 @@
}
function handleBlur() {
if (!title.trim()) {
isAdding = false;
if (title.trim()) {
handleSubmit();
}
isAdding = false;
}
function activate() {
isAdding = true;
}
$effect(() => {
if (isAdding && inputRef) {
inputRef.focus();
}
});
</script>
<div class="quick-add-inline">
<div class="inline-add-row" class:active={isAdding}>
<!-- Circle matching TaskItem checkbox style -->
{#if isAdding}
<div class="add-form p-3">
<input
bind:this={inputRef}
bind:value={title}
onkeydown={handleKeydown}
onblur={handleBlur}
{placeholder}
class="w-full px-0 py-1 text-sm bg-transparent outline-none text-foreground placeholder:text-muted-foreground"
autofocus
/>
<div class="flex justify-between items-center mt-2 pt-2 border-t border-border/50">
<button
class="px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground rounded-full hover:bg-primary/90 transition-all shadow-sm flex items-center gap-1.5"
onmousedown={(e) => e.preventDefault()}
onclick={handleSubmit}
>
<Plus size={14} />
{$t('kanban.add')}
</button>
<button
class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded-full transition-colors"
onmousedown={(e) => e.preventDefault()}
onclick={() => {
title = '';
isAdding = false;
}}
>
<X size={16} />
</button>
</div>
<div class="add-checkbox">
<Plus size={12} />
</div>
<input
bind:this={inputRef}
bind:value={title}
onkeydown={handleKeydown}
onblur={handleBlur}
{placeholder}
class="add-input"
/>
{:else}
<button
class="add-trigger group w-full p-2.5 text-sm text-muted-foreground hover:text-foreground transition-all flex items-center gap-2"
onclick={() => (isAdding = true)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="inline-add-trigger"
onclick={activate}
onkeydown={(e) => e.key === 'Enter' && activate()}
role="button"
tabindex="0"
>
<div
class="w-5 h-5 rounded-full border-2 border-dashed border-current group-hover:border-primary group-hover:text-primary flex items-center justify-center transition-colors"
>
<Plus size={14} />
<div class="add-checkbox placeholder">
<Plus size={12} />
</div>
<span class="group-hover:text-foreground transition-colors">{$t('kanban.addTask')}</span>
</button>
<span class="add-placeholder">{placeholder}</span>
</div>
{/if}
</div>
<style>
/* Glass-Pill add form */
.add-form {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 1rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
.inline-add-row {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.2rem 1rem;
transition: all 0.15s ease;
}
:global(.dark) .add-form {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
.inline-add-trigger {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
cursor: text;
}
/* Trigger button with subtle glass effect */
.add-trigger {
.add-checkbox {
width: 1.25rem;
height: 1.25rem;
border-radius: 9999px;
border: 2px dashed var(--color-muted-foreground);
opacity: 0.4;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--color-muted-foreground);
transition: all 0.15s;
}
.add-trigger:hover {
background: rgba(255, 255, 255, 0.5);
.inline-add-row.active .add-checkbox,
.inline-add-trigger:hover .add-checkbox {
opacity: 0.6;
border-color: var(--color-primary);
color: var(--color-primary);
}
:global(.dark) .add-trigger:hover {
background: rgba(255, 255, 255, 0.1);
.add-input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-size: 0.875rem;
color: var(--color-foreground);
padding: 0;
line-height: 1.5;
}
.add-input::placeholder {
color: var(--color-muted-foreground);
opacity: 0.6;
}
.add-placeholder {
font-size: 0.875rem;
color: var(--color-muted-foreground);
opacity: 0.6;
transition: all 0.15s;
}
.inline-add-trigger:hover .add-placeholder {
opacity: 0.8;
color: var(--color-foreground);
}
</style>

View file

@ -159,7 +159,7 @@
<style>
.board-page {
height: calc(100vh - 140px);
min-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
}

View file

@ -179,156 +179,160 @@
</div>
{/if}
<main class="flex-1 flex flex-col">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-12 max-[480px]:pt-8 px-4 pb-6 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 shadow-lg"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Form Section -->
<div class="flex-1 flex justify-center px-4 pt-4 pb-8">
<div
class="w-full max-w-[400px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<h2
class="text-xl font-semibold text-center mb-6"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
<main class="flex-1 flex flex-col items-center justify-center">
<div class="w-full max-w-[480px] mx-auto px-4 flex flex-col items-center">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-8 max-[480px]:pt-6 pb-4 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 shadow-lg"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
{mode === 'form' ? t.titleForm : t.titleSuccess}
</h2>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Error Messages -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
<!-- Form Section -->
<div class="w-full flex justify-center pt-2 pb-8">
<div
class="w-full max-w-[440px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<h2
class="text-xl font-semibold text-center mb-6"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
<p>{error}</p>
</div>
{/if}
{mode === 'form' ? t.titleForm : t.titleSuccess}
</h2>
<!-- Form Mode -->
{#if mode === 'form'}
<form
onsubmit={(e) => {
e.preventDefault();
handleForgotPassword();
}}
>
<p
class="mb-4 text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
<!-- Error Messages -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
{t.description}
</p>
<div class="mb-4">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
<p>{error}</p>
</div>
{/if}
<button
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
<!-- Form Mode -->
{#if mode === 'form'}
<form
onsubmit={(e) => {
e.preventDefault();
handleForgotPassword();
}}
>
<Key size={20} />
{loading ? t.sending : t.sendResetLinkButton}
</button>
</form>
<!-- Back Button -->
<div class="mt-4">
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center flex items-center justify-center gap-2 transition-opacity hover:opacity-70"
style:color={isDark ? '#fff' : '#000'}
>
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
<!-- Success Mode -->
{:else}
<div class="pb-4">
<div class="flex flex-col items-center mb-6">
<div
class="w-20 h-20 rounded-full flex items-center justify-center mb-6"
style:background-color={primaryColor + '30'}
style:color={primaryColor}
>
<EnvelopeOpen size={40} />
</div>
<p
class="text-sm text-center px-2"
class="mb-4 text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{getSuccessMessage(resetEmail)}
{t.description}
</p>
</div>
<div class="flex flex-col gap-3">
<div class="mb-4">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<button
type="button"
onclick={() => goto(loginPath)}
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
<SignIn size={20} />
{t.backToLogin}
<Key size={20} />
{loading ? t.sending : t.sendResetLinkButton}
</button>
</form>
<!-- Back Button -->
<div class="mt-4">
<button
type="button"
onclick={() => {
mode = 'form';
error = null;
}}
class="w-full h-10 border rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-70"
style:background-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
onclick={() => goto(loginPath)}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center flex items-center justify-center gap-2 transition-opacity hover:opacity-70"
style:color={isDark ? '#fff' : '#000'}
>
{t.resendEmail}
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
</div>
{/if}
<!-- Success Mode -->
{:else}
<div class="pb-4">
<div class="flex flex-col items-center mb-6">
<div
class="w-20 h-20 rounded-full flex items-center justify-center mb-6"
style:background-color={primaryColor + '30'}
style:color={primaryColor}
>
<EnvelopeOpen size={40} />
</div>
<p
class="text-sm text-center px-2"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{getSuccessMessage(resetEmail)}
</p>
</div>
<div class="flex flex-col gap-3">
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
<SignIn size={20} />
{t.backToLogin}
</button>
<button
type="button"
onclick={() => {
mode = 'form';
error = null;
}}
class="w-full h-10 border rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-70"
style:background-color={isDark
? 'rgba(255,255,255,0.1)'
: 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
>
{t.resendEmail}
</button>
</div>
</div>
{/if}
</div>
</div>
</div>
</main>
{#if appSlider}
<footer class="w-full pb-4 anim-fade-in">
<footer class="w-full max-w-[640px] mx-auto pb-4 anim-fade-in">
{@render appSlider()}
</footer>
{/if}

View file

@ -460,341 +460,315 @@
</div>
{/if}
<main class="flex-1 flex flex-col">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-12 max-[480px]:pt-8 px-4 pb-6 anim-fade-in-scale">
<button
type="button"
onclick={fillDevCredentials}
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105 active:scale-95"
class:success-pulse={showSuccess}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
aria-label="{appName} logo"
>
{#if showSuccess}
<Check size={55} class="text-green-500" />
{:else}
<Logo size={55} color={primaryColor} />
{/if}
</button>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Form Section -->
<div class="flex-1 flex justify-center px-4 pt-4 pb-8">
<div
class="w-full max-w-[400px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
class:shake={shakeError}
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
{#if showTwoFactor}
<!-- 2FA Verification -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.twoFactorTitle}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{useBackupCode ? t.twoFactorBackupSubtitle : t.twoFactorSubtitle}
</p>
</div>
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<Warning size={18} class="text-red-500 shrink-0" />
<p>{error}</p>
</div>
<main class="flex-1 flex flex-col items-center justify-center">
<div class="w-full max-w-[480px] mx-auto px-4 flex flex-col items-center">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-8 max-[480px]:pt-6 pb-4 anim-fade-in-scale">
<button
type="button"
onclick={fillDevCredentials}
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105 active:scale-95"
class:success-pulse={showSuccess}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
aria-label="{appName} logo"
>
{#if showSuccess}
<Check size={55} class="text-green-500" />
{:else}
<Logo size={55} color={primaryColor} />
{/if}
</button>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<form
onsubmit={(e) => {
e.preventDefault();
handleTwoFactorVerify();
}}
>
<div class="mb-3">
<input
type="text"
bind:value={twoFactorCode}
placeholder={useBackupCode ? t.twoFactorBackupPlaceholder : '000000'}
required
autocomplete="one-time-code"
inputmode={useBackupCode ? 'text' : 'numeric'}
maxlength={useBackupCode ? 20 : 6}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:text-align="center"
style:font-size="1.5rem"
style:letter-spacing="0.5rem"
style:--tw-ring-color="var(--ring-color)"
/>
<!-- Form Section -->
<div class="w-full flex justify-center pt-2 pb-8">
<div
class="w-full max-w-[440px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
class:shake={shakeError}
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
{#if showTwoFactor}
<!-- 2FA Verification -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.twoFactorTitle}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{useBackupCode ? t.twoFactorBackupSubtitle : t.twoFactorSubtitle}
</p>
</div>
{#if !useBackupCode}
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:margin-bottom="1rem"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<input
type="checkbox"
bind:checked={trustDevice}
style:accent-color={primaryColor}
/>
<span>{t.twoFactorTrustDevice}</span>
</label>
<Warning size={18} class="text-red-500 shrink-0" />
<p>{error}</p>
</div>
{/if}
<button
type="submit"
disabled={loading || !twoFactorCode}
aria-disabled={loading || !twoFactorCode}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
<form
onsubmit={(e) => {
e.preventDefault();
handleTwoFactorVerify();
}}
>
{loading ? t.twoFactorVerifying : t.twoFactorConfirm}
</button>
</form>
<div class="mb-3">
<input
type="text"
bind:value={twoFactorCode}
placeholder={useBackupCode ? t.twoFactorBackupPlaceholder : '000000'}
required
autocomplete="one-time-code"
inputmode={useBackupCode ? 'text' : 'numeric'}
maxlength={useBackupCode ? 20 : 6}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:text-align="center"
style:font-size="1.5rem"
style:letter-spacing="0.5rem"
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-4"
style:color={primaryColor}
onclick={() => {
useBackupCode = !useBackupCode;
twoFactorCode = '';
clearError();
}}
>
{useBackupCode ? t.twoFactorUseAuthenticator : t.twoFactorUseBackupCode}
</button>
{#if !useBackupCode}
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:margin-bottom="1rem"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<input
type="checkbox"
bind:checked={trustDevice}
style:accent-color={primaryColor}
/>
<span>{t.twoFactorTrustDevice}</span>
</label>
{/if}
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-2"
style:color={primaryColor}
onclick={() => {
showTwoFactor = false;
twoFactorCode = '';
useBackupCode = false;
clearError();
}}
>
{t.twoFactorBackToLogin}
</button>
{:else}
{#if showVerifiedBanner}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.emailVerified}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (showVerifiedBanner = false)}
aria-label="Close"
type="submit"
disabled={loading || !twoFactorCode}
aria-disabled={loading || !twoFactorCode}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
&times;
{loading ? t.twoFactorVerifying : t.twoFactorConfirm}
</button>
</div>
{/if}
</form>
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.title}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.subtitle}
</p>
</div>
{#if passkeyAvailable && onSignInWithPasskey}
<button
type="button"
onclick={handlePasskeySignIn}
disabled={loading || showSuccess}
aria-disabled={loading || showSuccess}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity bg-transparent hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:border-color={primaryColor}
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-4"
style:color={primaryColor}
onclick={() => {
useBackupCode = !useBackupCode;
twoFactorCode = '';
clearError();
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z" />
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
<span>Passkey</span>
{useBackupCode ? t.twoFactorUseAuthenticator : t.twoFactorUseBackupCode}
</button>
<div class="divider flex items-center gap-4 my-5">
<span
class="text-xs"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>{t.orDivider}</span
>
</div>
{/if}
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-2"
style:color={primaryColor}
onclick={() => {
showTwoFactor = false;
twoFactorCode = '';
useBackupCode = false;
clearError();
}}
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.verificationEmailSent}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (verificationEmailSent = false)}
aria-label="Close"
{t.twoFactorBackToLogin}
</button>
{:else}
{#if showVerifiedBanner}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
&times;
</button>
</div>
{/if}
{#if isLockedOut}
<div
class="flex gap-3 p-4 mb-4 rounded-xl bg-amber-500/15 border border-amber-500/30 text-amber-500"
role="alert"
aria-live="assertive"
>
<div class="shrink-0 mt-0.5">
<Warning size={24} />
</div>
<div class="flex flex-col gap-1">
<p class="font-semibold text-[0.9rem]">{t.accountLocked}</p>
<p class="text-[0.8rem] opacity-90">
{t.tooManyAttempts}
{#if rateLimitCountdown > 0}
{t.retryIn} <strong>{formatCountdown(rateLimitCountdown)}</strong>
{/if}
</p>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.emailVerified}</p>
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium text-[0.8rem] p-0 text-left underline mt-1"
onclick={() => goto(forgotPasswordPath)}
style:color={primaryColor}
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (showVerifiedBanner = false)}
aria-label="Close"
>
{t.resetPassword}
&times;
</button>
</div>
{/if}
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.title}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.subtitle}
</p>
</div>
{:else if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
id="form-error"
role="alert"
aria-live="assertive"
>
<Warning size={18} class="text-red-500 shrink-0" />
<div class="flex flex-col gap-1">
<p>{error}</p>
{#if showEmailNotVerified && onResendVerification}
{#if passkeyAvailable && onSignInWithPasskey}
<button
type="button"
onclick={handlePasskeySignIn}
disabled={loading || showSuccess}
aria-disabled={loading || showSuccess}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity bg-transparent hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:border-color={primaryColor}
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z" />
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
<span>Passkey</span>
</button>
<div class="divider flex items-center gap-4 my-5">
<span
class="text-xs"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>{t.orDivider}</span
>
</div>
{/if}
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.verificationEmailSent}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (verificationEmailSent = false)}
aria-label="Close"
>
&times;
</button>
</div>
{/if}
{#if isLockedOut}
<div
class="flex gap-3 p-4 mb-4 rounded-xl bg-amber-500/15 border border-amber-500/30 text-amber-500"
role="alert"
aria-live="assertive"
>
<div class="shrink-0 mt-0.5">
<Warning size={24} />
</div>
<div class="flex flex-col gap-1">
<p class="font-semibold text-[0.9rem]">{t.accountLocked}</p>
<p class="text-[0.8rem] opacity-90">
{t.tooManyAttempts}
{#if rateLimitCountdown > 0}
{t.retryIn} <strong>{formatCountdown(rateLimitCountdown)}</strong>
{/if}
</p>
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium text-sm p-0 text-left underline hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
class="bg-transparent border-none cursor-pointer font-medium text-[0.8rem] p-0 text-left underline mt-1"
onclick={() => goto(forgotPasswordPath)}
style:color={primaryColor}
>
{resendingVerification ? t.resendingVerification : t.resendVerification}
{t.resetPassword}
</button>
{/if}
{#if rateLimitCountdown > 0}
<p class="font-semibold mt-1">
{t.retryIn}
{formatCountdown(rateLimitCountdown)}
</p>
{/if}
</div>
</div>
</div>
{/if}
{:else if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
id="form-error"
role="alert"
aria-live="assertive"
>
<Warning size={18} class="text-red-500 shrink-0" />
<div class="flex flex-col gap-1">
<p>{error}</p>
{#if showEmailNotVerified && onResendVerification}
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium text-sm p-0 text-left underline hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
style:color={primaryColor}
>
{resendingVerification ? t.resendingVerification : t.resendVerification}
</button>
{/if}
{#if rateLimitCountdown > 0}
<p class="font-semibold mt-1">
{t.retryIn}
{formatCountdown(rateLimitCountdown)}
</p>
{/if}
</div>
</div>
{/if}
<form
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
aria-busy={loading}
>
<!-- Email -->
<div class="mb-3">
<label for="email" class="sr-only">{t.emailPlaceholder}</label>
<input
id="email"
type="email"
bind:this={emailInput}
bind:value={email}
placeholder={t.emailPlaceholder}
required
autocomplete={passkeyAvailable ? 'username webauthn' : 'email'}
aria-invalid={errorField === 'email'}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'email'}
style:--ring-color={errorField === 'email' ? '#ef4444' : primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={errorField === 'email'
? '#ef4444'
: isDark
? 'rgba(255,255,255,0.2)'
: 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<!-- Password -->
<div class="mb-3">
<label for="password" class="sr-only">{t.passwordPlaceholder}</label>
<div class="relative">
<form
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
aria-busy={loading}
>
<!-- Email -->
<div class="mb-3">
<label for="email" class="sr-only">{t.emailPlaceholder}</label>
<input
id="password"
type={showPassword ? 'text' : 'password'}
bind:this={passwordInput}
bind:value={password}
placeholder={t.passwordPlaceholder}
id="email"
type="email"
bind:this={emailInput}
bind:value={email}
placeholder={t.emailPlaceholder}
required
autocomplete="current-password"
aria-invalid={errorField === 'password'}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'password'}
style:--ring-color={errorField === 'password' ? '#ef4444' : primaryColor}
autocomplete={passkeyAvailable ? 'username webauthn' : 'email'}
aria-invalid={errorField === 'email'}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'email'}
style:--ring-color={errorField === 'email' ? '#ef4444' : primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={errorField === 'password'
style:border-color={errorField === 'email'
? '#ef4444'
: isDark
? 'rgba(255,255,255,0.2)'
@ -802,150 +776,178 @@
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<!-- Password -->
<div class="mb-3">
<label for="password" class="sr-only">{t.passwordPlaceholder}</label>
<div class="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
bind:this={passwordInput}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
autocomplete="current-password"
aria-invalid={errorField === 'password'}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'password'}
style:--ring-color={errorField === 'password' ? '#ef4444' : primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={errorField === 'password'
? '#ef4444'
: isDark
? 'rgba(255,255,255,0.2)'
: 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity hover:opacity-80"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<!-- Remember & Forgot -->
<div class="flex justify-between items-center mb-4 text-sm">
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<input
type="checkbox"
bind:checked={rememberMe}
style:accent-color={primaryColor}
/>
<span>{t.rememberMe}</span>
</label>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity hover:opacity-80"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
onclick={() => goto(forgotPasswordPath)}
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
style:color={primaryColor}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
{t.forgotPassword}
</button>
</div>
</div>
<!-- Remember & Forgot -->
<div class="flex justify-between items-center mb-4 text-sm">
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<input
type="checkbox"
bind:checked={rememberMe}
style:accent-color={primaryColor}
/>
<span>{t.rememberMe}</span>
</label>
<!-- Submit -->
<button
type="button"
onclick={() => goto(forgotPasswordPath)}
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
style:color={primaryColor}
type="submit"
disabled={loading || showSuccess || rateLimitCountdown > 0}
aria-disabled={loading || showSuccess || rateLimitCountdown > 0}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={showSuccess ? '#22c55e' : primaryColor + '60'}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{t.forgotPassword}
{#if loading}
<svg
class="spinner"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.signingIn}</span>
{:else if showSuccess}
<Check size={20} />
<span>{t.success}</span>
{:else}
<SignIn size={20} />
<span>{t.signInButton}</span>
{/if}
</button>
</div>
</form>
<!-- Submit -->
<button
type="submit"
disabled={loading || showSuccess || rateLimitCountdown > 0}
aria-disabled={loading || showSuccess || rateLimitCountdown > 0}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={showSuccess ? '#22c55e' : primaryColor + '60'}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{#if loading}
<svg
class="spinner"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
{#if onSendMagicLink}
{#if magicLinkSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.signingIn}</span>
{:else if showSuccess}
<Check size={20} />
<span>{t.success}</span>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.magicLinkSent?.replace('{email}', email)}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (magicLinkSent = false)}
aria-label="Close"
>
&times;
</button>
</div>
{:else}
<SignIn size={20} />
<span>{t.signInButton}</span>
{/if}
</button>
</form>
{#if onSendMagicLink}
{#if magicLinkSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.magicLinkSent?.replace('{email}', email)}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (magicLinkSent = false)}
aria-label="Close"
onclick={handleSendMagicLink}
disabled={sendingMagicLink || !email}
aria-disabled={sendingMagicLink || !email}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center transition-opacity hover:opacity-70 disabled:opacity-40 disabled:cursor-not-allowed"
style:color={primaryColor}
>
&times;
{sendingMagicLink ? t.magicLinkSending : t.magicLinkButton}
</button>
</div>
{:else}
{/if}
{/if}
<p
class="text-center text-sm mt-4"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.noAccount}
<button
type="button"
onclick={handleSendMagicLink}
disabled={sendingMagicLink || !email}
aria-disabled={sendingMagicLink || !email}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center transition-opacity hover:opacity-70 disabled:opacity-40 disabled:cursor-not-allowed"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
onclick={() => goto(registerPath)}
style:color={primaryColor}
>
{sendingMagicLink ? t.magicLinkSending : t.magicLinkButton}
{t.createAccount}
</button>
{/if}
</p>
{/if}
<p
class="text-center text-sm mt-4"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.noAccount}
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
onclick={() => goto(registerPath)}
style:color={primaryColor}
>
{t.createAccount}
</button>
</p>
{/if}
</div>
</div>
{#if version}
<p
class="text-[10px] text-gray-400/60 select-none pointer-events-none m-0 pt-2 pb-1 text-center"
>
v{version}{#if buildTime}
· {new Date(buildTime).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})}
{new Date(buildTime).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
})}{/if}
</p>
{/if}
</div>
</main>
{#if appSlider}
<footer class="w-full pb-4 anim-fade-in">
<footer class="w-full max-w-[640px] mx-auto pb-4 anim-fade-in">
{@render appSlider()}
</footer>
{/if}
{#if version}
<p
class="fixed bottom-2 right-3 text-[10px] text-gray-400/60 select-none pointer-events-none m-0"
>
v{version}{#if buildTime}
· {new Date(buildTime).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})}
{new Date(buildTime).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
})}{/if}
</p>
{/if}
</div>
<style>

View file

@ -286,321 +286,323 @@
</div>
{/if}
<main class="flex-1 flex flex-col">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-12 max-[480px]:pt-8 px-4 pb-6 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Form Section -->
<div class="flex-1 flex justify-center px-4 pt-4 pb-8">
<div
class="w-full max-w-[400px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.title}
</h2>
<main class="flex-1 flex flex-col items-center justify-center">
<div class="w-full max-w-[480px] mx-auto px-4 flex flex-col items-center">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-8 max-[480px]:pt-6 pb-4 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Error Message -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<span>&#9888;</span>
<p>{error}</p>
</div>
{/if}
<!-- Verification Email Sent -->
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl text-sm bg-green-500/15 border border-green-500/30 text-green-500"
>
<span>{t.verificationEmailSent}</span>
</div>
{/if}
<!-- Success: Needs Verification / Already Registered -->
{#if success && needsVerification}
<div
class="mb-6 rounded-xl p-5 border-2"
class:bg-green-500={false}
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 15%, transparent)'
: 'color-mix(in srgb, #22c55e 15%, transparent)'}
style:border-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 40%, transparent)'
: 'color-mix(in srgb, #22c55e 40%, transparent)'}
>
<div class="flex items-start gap-3 mb-4">
<div
class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 20%, transparent)'
: 'color-mix(in srgb, #22c55e 20%, transparent)'}
>
{#if emailAlreadyRegistered}
<svg
class="w-5 h-5"
style:color="#f59e0b"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
{:else}
<svg
class="w-5 h-5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
{/if}
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style:color={emailAlreadyRegistered
? isDark
? '#fbbf24'
: '#d97706'
: isDark
? '#22c55e'
: '#16a34a'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegistered || 'Email already registered'
: t.checkYourEmail || 'Check your email'}
</h3>
<p
class="text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegisteredMessage ||
"An account with this email already exists. If you haven't verified your email yet, resend the verification email."
: t.accountCreated}
</p>
</div>
</div>
<div
class="pt-3 border-t flex flex-col gap-2"
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
<!-- Form Section -->
<div class="w-full flex justify-center pt-2 pb-8">
<div
class="w-full max-w-[440px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{#if onResendVerification}
<button
type="button"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed border-[1.5px]"
style:background-color="{primaryColor}40"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
{t.title}
</h2>
</div>
<!-- Error Message -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<span>&#9888;</span>
<p>{error}</p>
</div>
{/if}
<!-- Verification Email Sent -->
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl text-sm bg-green-500/15 border border-green-500/30 text-green-500"
>
<span>{t.verificationEmailSent}</span>
</div>
{/if}
<!-- Success: Needs Verification / Already Registered -->
{#if success && needsVerification}
<div
class="mb-6 rounded-xl p-5 border-2"
class:bg-green-500={false}
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 15%, transparent)'
: 'color-mix(in srgb, #22c55e 15%, transparent)'}
style:border-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 40%, transparent)'
: 'color-mix(in srgb, #22c55e 40%, transparent)'}
>
<div class="flex items-start gap-3 mb-4">
<div
class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 20%, transparent)'
: 'color-mix(in srgb, #22c55e 20%, transparent)'}
>
{#if resendingVerification}
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
fill="none"
></circle>
{#if emailAlreadyRegistered}
<svg
class="w-5 h-5"
style:color="#f59e0b"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
{t.resendingVerification}
{:else}
{t.resendVerification}
<svg
class="w-5 h-5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
{/if}
</button>
{/if}
{#if emailAlreadyRegistered}
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style:color={emailAlreadyRegistered
? isDark
? '#fbbf24'
: '#d97706'
: isDark
? '#22c55e'
: '#16a34a'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegistered || 'Email already registered'
: t.checkYourEmail || 'Check your email'}
</h3>
<p
class="text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegisteredMessage ||
"An account with this email already exists. If you haven't verified your email yet, resend the verification email."
: t.accountCreated}
</p>
</div>
</div>
<div
class="pt-3 border-t flex flex-col gap-2"
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
{#if onResendVerification}
<button
type="button"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed border-[1.5px]"
style:background-color="{primaryColor}40"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{#if resendingVerification}
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
fill="none"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{t.resendingVerification}
{:else}
{t.resendVerification}
{/if}
</button>
{/if}
{#if emailAlreadyRegistered}
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full flex items-center justify-center h-11 rounded-lg font-medium transition-opacity hover:opacity-80 border-[1.5px]"
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
style:background-color="transparent"
>
{t.goToLogin || 'Sign in instead'}
</button>
{/if}
</div>
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
>
<!-- Email -->
<div class="mb-3">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
</div>
<!-- Password -->
<div class="mb-3">
<div class="relative">
<input
type={showPassword ? 'text' : 'password'}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full flex items-center justify-center h-11 rounded-lg font-medium transition-opacity hover:opacity-80 border-[1.5px]"
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
style:background-color="transparent"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{t.goToLogin || 'Sign in instead'}
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
{/if}
</div>
</div>
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
>
<!-- Email -->
<div class="mb-3">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
<PasswordStrength {password} {primaryColor} />
<!-- Confirm Password -->
<div class="mb-3">
<div class="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
bind:value={confirmPassword}
placeholder={t.confirmPasswordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
>
{#if showConfirmPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<!-- Password Requirements -->
<p
class="text-xs mb-3"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>
{t.passwordRequirements}
</p>
<!-- Submit -->
<button
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color="{primaryColor}60"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
>
{#if loading}
<svg
class="w-5 h-5 animate-spin"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.creatingAccount}</span>
{:else}
<UserPlus size={20} />
<span>{t.createAccountButton}</span>
{/if}
</button>
</form>
<!-- Back to Login -->
<div class="text-center mt-4">
<button
type="button"
onclick={() => goto(loginPath)}
class="inline-flex items-center gap-2 bg-transparent border-none cursor-pointer font-medium transition-opacity hover:opacity-70"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
<!-- Password -->
<div class="mb-3">
<div class="relative">
<input
type={showPassword ? 'text' : 'password'}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<PasswordStrength {password} {primaryColor} />
<!-- Confirm Password -->
<div class="mb-3">
<div class="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
bind:value={confirmPassword}
placeholder={t.confirmPasswordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
>
{#if showConfirmPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<!-- Password Requirements -->
<p
class="text-xs mb-3"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>
{t.passwordRequirements}
</p>
<!-- Submit -->
<button
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color="{primaryColor}60"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{#if loading}
<svg
class="w-5 h-5 animate-spin"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.creatingAccount}</span>
{:else}
<UserPlus size={20} />
<span>{t.createAccountButton}</span>
{/if}
</button>
</form>
<!-- Back to Login -->
<div class="text-center mt-4">
<button
type="button"
onclick={() => goto(loginPath)}
class="inline-flex items-center gap-2 bg-transparent border-none cursor-pointer font-medium transition-opacity hover:opacity-70"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
</div>
</div>
@ -608,7 +610,7 @@
<!-- App Slider -->
{#if appSlider}
<footer class="w-full pb-4 anim-fade-in">
<footer class="w-full max-w-[640px] mx-auto pb-4 anim-fade-in">
{@render appSlider()}
</footer>
{/if}