Ergänzt fehlende i18n-Keys (Himmelsrichtungen, Platzhalter, Schrittweite) und zeigt Validierungsfehler im Kurs-Dial. Co-authored-by: Cursor <cursoragent@cursor.com>
12 KiB
Implementierungsplan: 360°-Kompass-Dial für Kursangaben
Status: Implementiert (Branch feat/compass-course-dial)
Bezug: Ereignisprotokoll (LogEntryEditor), Felder MgK / rwK / Windrichtung
Vorbild im Projekt: EventTimeInput24h (spezialisierte Eingabe + Text-Fallback, keine API-Änderung)
1. Ziel und Nicht-Ziele
Ziel
- Eingabe von Kurswinkeln (0°–360°) über einen mobil tauglichen Kompass-Ring (Drag/Tap).
- Hybrid-Eingabe: Dial + numerisches Feld (wie bei der Uhrzeit).
- Einheitliche Normalisierung (
000–360, Speicherung als String ohne°). - Wiederverwendbare Komponente für MgK, rwK und optional Wind (Gradmodus).
Nicht-Ziele (v1)
- Keine Änderung am Server-Schema oder Verschlüsselungsformat.
- Keine Device-Orientation / echter Kompass des Geräts (optional Phase 2).
- Kein Ersatz der Ablenkungstabelle (
DeviationForm) – bleibt 10°-Raster. - Windrichtung bleibt kompatibel mit bestehenden Kardinalwerten (
N,NNE, …) aus Wetter-API.
2. Ist-Analyse
| Feld | Speicherformat | UI heute | Besonderheit |
|---|---|---|---|
mgk |
String, z. B. "042" |
Text placeholder="e.g. 180" |
Grad, PDF/CSV mit ° |
rwk |
String, z. B. "038" |
Text | Grad |
windDirection |
String | Text | Oft Kardinal (NW) via OpenWeather; manuell auch Grad möglich |
Betroffene Dateien (Lesen/Schreiben, unverändert speichern):
client/src/components/LogEntryEditor.tsx– Formular + Tabelleclient/src/utils/logEntryPayload.ts–normalizeLogEventclient/src/services/pdfExport.ts,csvExport.ts– Exportclient/src/services/demoLogbookData.ts– Demo-Daten
Referenz-Pattern: EventTimeInput24h.tsx + parseTimeToHHMM / joinTimeHHMM in logEntryPayload.ts.
3. Architektur
client/src/utils/courseAngle.ts # Parsing, Normalisierung, Winkel-Mathe
client/src/components/CourseDialInput.tsx # UI: SVG-Ring + Zahleneingabe
client/src/components/CourseDialField.tsx # Label + Fehler + Modus (optional)
client/src/App.css # .course-dial-* Styles
client/src/components/LogEntryEditor.tsx # Integration MgK/rwk/Wind
client/src/i18n/locales/{de,en}.json # Strings
3.1 Utility-Schicht courseAngle.ts
| Funktion | Verhalten |
|---|---|
parseCourseAngle(value) |
"185", "185°", " 042 " → 185 oder null |
formatCourseAngle(degrees, pad?) |
185 → "185" oder "185" / "042" (pad optional) |
normalizeCourseAngleString(value) |
Parse oder Fallback; für normalizeLogEvent |
pointerAngleToDegrees(clientX, clientY, cx, cy) |
atan2, 0° = Nord, Uhrzeigersinn maritim |
degreesToCardinal(deg) |
16-Sektoren (bestehende Logik aus Wetter-Import) |
cardinalToDegrees(label) |
Reverse für Dial-Anzeige bei Kardinal-Strings |
snapDegrees(deg, step) |
step 1, 5 oder 10 |
Konvention: 0° = Nord, Winkel im Uhrzeigersinn (Kompass/Navigation), konsistent mit wind.deg in LogEntryEditor.
3.2 Komponente CourseDialInput
Props:
interface CourseDialInputProps {
value: string // roher Formularwert
onChange: (value: string) => void
disabled?: boolean
step?: 1 | 5 | 10 // Standard: 1
allowCardinal?: boolean // Wind: true → Anzeige/Export Kardinal optional
displayMode?: 'degrees' | 'cardinal' | 'auto'
'aria-label': string
id?: string
}
UI-Aufbau:
- SVG-Ring (ca. 200–240 px Desktop, min. 160 px Mobile)
- Gradmarken alle 30° (Labels 000, 030, … 330)
- Zeiger / Highlight-Bogen bei aktuellem Wert
touch-action: noneauf Ringfläche
- Zentrum: große Anzeige
185°oderNW - Darunter:
<input type="text" inputMode="numeric">mit Validierung on blur - Fein/Grob-Toggle (optional): 1° / 5° / 10° (lokal in
sessionStoragemerken)
Interaktion:
pointerdown→setPointerCapture→pointermove→ Winkel berechnen → snappen →onChange- Tap auf Ring: Winkel zum Tap-Punkt
- Tastatur am Zahleneingang: Pfeiltasten ±step (wenn fokussiert)
Barrierefreiheit:
role="slider",aria-valuemin={0},aria-valuemax={360},aria-valuenow,aria-label- Zahleneingang bleibt voll bedienbar ohne Dial
- Fokus-Reihenfolge: Input vor Dial oder umgekehrt (Input zuerst empfohlen)
3.3 Windrichtung: Modus-Entscheidung
Empfehlung v1: Zwei Darstellungsmodi, ein Speicher-String:
| Modus | Speicher | Dial |
|---|---|---|
| Grad | "225" |
Standard-Dial |
| Kardinal | "SW" |
Dial zeigt Sektor-Mitte (225°), Änderung schreibt Kardinal |
- Wetter-Import (
handleFetchWeather) setzt weiter Kardinal → Dial mappt auf Sektor. - Nutzer kann auf Grad umschalten (kleiner Link „Als Grad“ / Toggle).
normalizeLogEvent: erkennt Kardinal vs. Zahl, keine erzwungene Konvertierung beim Laden.
4. Integration LogEntryEditor
4.1 Layout (mobil-first)
Problem: Formular ist bereits dicht (form-grid).
Lösung: Kurs-Block als eigene Sektion „Kurs“ mit Tabs:
[ MgK ] [ rwK ] ← Tab-Leiste (Segmented Control)
┌─────────────────────────┐
│ CourseDialInput │ ← ein Dial, Wert je Tab
│ + Zahleneingang │
└─────────────────────────┘
- Ein Dial, State wechselt mit Tab (
activeCourseField: 'mgk' | 'rwk'). - Spart Platz; MgK/rwk werden nacheinander gesetzt (typischer Workflow).
Windrichtung: eigene Zeile unter Wetter-Grid; kompakter Dial (kleinere size="sm") oder ausklappbar „Wind am Kompass setzen“.
4.2 Ersetzungen
| Alt | Neu |
|---|---|
<input> MgK |
<CourseDialInput value={evMgk} … /> |
<input> rwK |
Tab + gleicher Dial |
<input> Wind |
<CourseDialInput allowCardinal displayMode="auto" … /> |
4.3 normalizeLogEvent
mgk: normalizeCourseAngleString(e.mgk, { allowEmpty: true }),
rwk: normalizeCourseAngleString(e.rwk, { allowEmpty: true }),
windDirection: normalizeWindDirectionString(e.windDirection), // Kardinal ODER Grad-String
Bestehende Demo- und Export-Daten bleiben gültig.
5. Styling (App.css)
.course-dial– Container, max-width, zentriert.course-dial__svg–width: 100%; aspect-ratio: 1.course-dial__ring– stroke, hover/active.course-dial__needle– transformrotate(${deg}deg).course-dial__value– tabular-nums, große Schrift.course-dial__input– wie.time-input-24h.course-dial-tabs– Segmented Control (bestehende--app-accent-*Tokens)- Responsive:
@media (max-width: 640px)– Dial max min(72vw, 220px); Touch-Target Ring ≥ 44 px
Theme: currentColor / CSS-Variablen (--app-text, --app-accent-light) – Dark/Light via themes.css.
6. Internationalisierung
Neue Keys unter logs.*:
| Key | DE | EN |
|---|---|---|
course_dial_hint |
Am Ring drehen oder Grad eingeben | Drag the ring or enter degrees |
course_step_fine |
1° | 1° |
course_step_medium |
5° | 5° |
course_step_coarse |
10° | 10° |
course_tab_mgk |
MgK | MgK |
course_tab_rwk |
rwK | rwK |
course_invalid |
Ungültiger Kurs (0–360) | Invalid course (0–360) |
wind_mode_cardinal |
Kardinal | Cardinal |
wind_mode_degrees |
Grad | Degrees |
7. Phasen und Aufwand
Phase A – Fundament (1–1,5 Tage)
courseAngle.ts+ Unit-Tests (Vitest einrichten falls noch nicht vorhanden)CourseDialInput(nur Grad, step 1/5, Pointer + Input)- CSS Grundlayout
- Story/manuell: isoliert in kleiner Demo-Route oder Storybook (optional)
Akzeptanz: Dial setzt 0–360, Input synchron, Mobile Chrome/Safari getestet.
Phase B – LogEntryEditor MgK/rwk (1 Tag)
- Tab-UI MgK / rwK
- Integration,
normalizeLogEvent - Read-only: Dial disabled, Wert nur Anzeige
Akzeptanz: Ereignis speichern/laden/PDF unverändert korrekt; Skipper-Signatur-Flow unberührt.
Phase C – Windrichtung (0,5–1 Tag)
allowCardinal/displayMode- Wetter-Import kompatibel
- Toggle Kardinal ↔ Grad
Akzeptanz: API-Wind NW zeigt Dial auf NW; manuelle Grad-Eingabe möglich.
Phase D – Polish (1–1,5 Tage)
- Fein/Grob-Schritte + Persistenz
- Tastatur (Pfeiltasten), Fokus-Stile
- Reduzierte Bewegung (
prefers-reduced-motion: nur Input, Dial statisch) - Plausible-Event optional:
Course Dial Used(nur wenn Analytics gewünscht) - Dokumentation in
docs/plausible-events.mdfalls Event
Phase E – QA & Edge Cases (0,5 Tag)
- Leerer Wert, 360 → 0 oder 360 (festlegen: 360 als Eingabe → speichern
360oder000– Empfehlung: intern 0–359 speichern, Anzeige 360 = 0) - Sehr lange Formulare auf kleinen Screens (Scroll, kein Layout-Sprung)
- Offline/PWA, kein Regression bei
buildLogEntryPayload/ Signatur-Hash
Gesamtaufwand: ca. 4–5 Entwicklertage für vollständige Implementierung inkl. Wind + A11y + QA.
8. Tests
Unit (courseAngle.ts)
- Parse:
"042","360","999"(invalid),"NW"(wind helper) pointerAngleToDegreesmit festen KoordinatensnapDegrees(47, 5)→ 45degreesToCardinal/cardinalToDegreesRoundtrip
Komponente (Testing Library)
onChangebei simuliertem Pointer-Event (oder direktemsetValuevia Input)- Disabled-State
aria-valuenowaktualisiert
Manuell / UAT
| # | Schritt | Erwartung |
|---|---|---|
| 1 | Neues Ereignis, MgK am Dial auf 090 | Tabelle zeigt 90°, PDF/CSV 90 |
| 2 | rwK per Tastatur 270 |
Dial zeigt West |
| 3 | Wetter laden | Wind NW, Dial passend |
| 4 | iPhone Safari, Daumen-Drag | Kein Scroll-Leaken, Wert stabil |
| 5 | Nur Tastatur | Input allein speicherbar |
| 6 | Bestehenden Eintrag bearbeiten | Alte Werte korrekt im Dial |
9. Risiken und Mitigationen
| Risiko | Mitigation |
|---|---|
| Dial zu groß auf Mobile | Tabs + max-width; Wind einklappbar |
| Scroll vs. Drag | touch-action: none nur am Ring |
| Kardinal/Grad-Inkonsistenz | displayMode="auto", kein Silent-Overwrite |
| Signatur-Hash ändert sich | Nur Normalisierung die bereits gültige Strings erlaubt; keine Rundung beim Speichern ohne Nutzeraktion |
| Performance bei vielen Events | Dial nur im Formular, nicht in Tabelle |
10. Optionale Erweiterungen (Post-v1)
- MgK → rwK aus Ablenkungstabelle vorschlagen (Lookup
deviations[roundedMgK]). - DeviceOrientation für Ring-Ausrichtung (mit Permission-Hinweis).
- Haptik
navigator.vibrate(10)bei Snap (Android). - DeviationForm: visueller Kompass statt nur Grid (separate Story).
11. Abnahmekriterien (Definition of Done)
- MgK und rwK im Ereignisformular per Dial + Input editierbar (Desktop + Mobile).
- Windrichtung: Dial + Kardinal/Grad kompatibel mit Wetter-Import.
- Keine Backend-/Migrations-Änderung; bestehende Logbücher laden unverändert.
- PDF/CSV/Signatur-Verhalten identisch zu heute (nur Darstellung/Eingabe verbessert).
- WCAG: Slider + Input bedienbar,
prefers-reduced-motionberücksichtigt. - DE/EN vollständig übersetzt.
12. Empfohlene Umsetzungsreihenfolge (Commits)
feat(course): add courseAngle utilities and testsfeat(course): add CourseDialInput component and stylesfeat(logs): integrate compass dial for MgK and rwKfeat(logs): wind direction dial with cardinal supportfix(logs): a11y and reduced-motion for course dialdocs: compass course dial plan and plausible event(optional)