From d8503bc54be81ba3798881905a74e72a42fbeebd Mon Sep 17 00:00:00 2001 From: Ulas Date: Wed, 1 Apr 2026 11:42:15 +0200 Subject: [PATCH] chore: exclude docs/superpowers/ from version control Internal Claude Code working documents (plans, specs) are not relevant for contributors. Remove tracked files and add to .gitignore. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 + .../superpowers/plans/2026-03-30-ux-polish.md | 1545 ----------------- .../2026-03-31-shopping-swipe-gestures.md | 476 ----- .../specs/2026-03-30-ux-polish-design.md | 224 --- .../2026-03-31-module-accent-colors-design.md | 73 - ...26-03-31-shopping-swipe-gestures-design.md | 79 - 6 files changed, 3 insertions(+), 2397 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-30-ux-polish.md delete mode 100644 docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md delete mode 100644 docs/superpowers/specs/2026-03-30-ux-polish-design.md delete mode 100644 docs/superpowers/specs/2026-03-31-module-accent-colors-design.md delete mode 100644 docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md diff --git a/.gitignore b/.gitignore index 8b3bdd3..e58a4ac 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ dist/ # Git Worktrees .worktrees/ +# Claude Code Arbeitsdokumente (interne Pläne/Specs, nicht für Contributors) +docs/superpowers/ + # Textdateien mit Tokens/Keys (Sicherheitsnetz) *.txt !public/robots.txt diff --git a/docs/superpowers/plans/2026-03-30-ux-polish.md b/docs/superpowers/plans/2026-03-30-ux-polish.md deleted file mode 100644 index ff15124..0000000 --- a/docs/superpowers/plans/2026-03-30-ux-polish.md +++ /dev/null @@ -1,1545 +0,0 @@ -# UX Polish — Implementierungsplan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Die vier UX-Schichten aus dem Design-Spec umsetzen: Konsistenz, Animationen, Mobile-Feel und Formular-UX. - -**Architecture:** Alle Änderungen sind rein frontend-seitig (CSS + Vanilla JS). Kein Backend betroffen. Jede Schicht ist ein eigener Commit-Block. Tests via `npm test` validieren weiterhin die Backend-Schicht; für neue JS-Utilities werden Tests in `test-ux-utils.js` angelegt. - -**Tech Stack:** Vanilla JS (ES modules), CSS Custom Properties, Web Animations API, Vibration API, `scrollIntoView`, `beforeinstallprompt` - ---- - -## Ist-Analyse (Stand v0.1.0) - -**Bereits erledigt — nicht anfassen:** -- Warm-Neutraltöne in `tokens.css` ✓ -- Modul-Akzentfarben als CSS-Tokens ✓ -- Typografie-Skala (Tokens) ✓ -- `letter-spacing` auf `.page__title` ✓ -- Skeleton-Shimmer in `layout.css` ✓ -- Skeleton-Loading auf Dashboard ✓ -- Skeleton in Shopping + Tasks ✓ -- `overscroll-behavior` in `pwa.css` ✓ -- `-webkit-overflow-scrolling` in `pwa.css` ✓ -- Safe-Area-Insets ✓ -- PWA-Banner (Slide-in) in `oikos-install-prompt.js` ✓ -- Theme-color pro Modul in `router.js` ✓ -- Focus-Trap, Escape, Overlay-Click in `modal.js` ✓ -- Auto-Focus (erster Fokus-Wechsel, 50ms) in `modal.js` ✓ -- Vibration in `tasks.js` (partiell) ✓ - -**Echte Lücken — werden umgesetzt:** -1. Schicht 1: Card-Padding inkonsistent in mehreren Modulen; FAB-Farben nicht überall mit Modul-Akzent verknüpft -2. Schicht 2: Seitenübergang nutzt `translateY` (vertikal) statt direktionaler `translateX`; kein Staggered Fade-In; Empty States nutzen modul-eigene CSS-Klassen statt `.empty-state` -3. Schicht 3: Install-Prompt erscheint sofort (kein Delay); Dismiss-Dauer 30d statt 7d; kein `scrollIntoView` bei virtuellem Keyboard; Vibration nur in tasks -4. Schicht 4: Kein Enter-to-Next-Field; keine Blur-Validierung; kein Bottom Sheet auf Mobile; kein Submit-Feedback (Checkmark/Shake) - ---- - -## Dateiübersicht - -| Datei | Aktion | Schicht | -|-------|--------|---------| -| `public/styles/layout.css` | Modify — Card-Padding-Token, FAB-Accent-Klasse, Bottom-Sheet-Styles, Slide-Animation | 1, 2, 4 | -| `public/styles/tasks.css` | Modify — Card-Padding anpassen, Swipe-Reveal-Opacity | 1, 2 | -| `public/styles/contacts.css` | Modify — Card-Padding, Empty-State-Klasse entfernen | 1, 2 | -| `public/styles/budget.css` | Modify — Card-Padding, Empty-State-Klasse entfernen | 1, 2 | -| `public/styles/notes.css` | Modify — Empty-State-Klasse entfernen | 2 | -| `public/styles/shopping.css` | Modify — Empty-State-Klasse entfernen | 2 | -| `public/styles/meals.css` | Modify — Card-Padding | 1 | -| `public/router.js` | Modify — `translateX`-Seitenübergang (directional) | 2 | -| `public/pages/tasks.js` | Modify — Stagger, Swipe-Reveal-Opacity, Vibration-Utility | 2, 3 | -| `public/pages/shopping.js` | Modify — Stagger, Empty-State-Klasse | 2 | -| `public/pages/meals.js` | Modify — Stagger, Empty-State | 2 | -| `public/pages/contacts.js` | Modify — Stagger, Empty-State-Klasse | 2 | -| `public/pages/budget.js` | Modify — Stagger, Empty-State-Klasse | 2 | -| `public/pages/notes.js` | Modify — Stagger, Empty-State-Klasse | 2 | -| `public/pages/calendar.js` | Modify — Stagger | 2 | -| `public/components/oikos-install-prompt.js` | Modify — Timing (Delay), Dismiss 7d | 3 | -| `public/components/modal.js` | Modify — Bottom Sheet, scrollIntoView, Enter-Nav, Blur-Validierung, Submit-Feedback | 3, 4 | -| `public/utils/ux.js` | Create — `stagger()`, `vibrate()` Utilities | 2, 3 | -| `test-ux-utils.js` | Create — Tests für `stagger()` und `vibrate()` | 2, 3 | - ---- - -## Schicht 1 — Design-Sprache & Konsistenz - -### Task 1: Card-Padding vereinheitlichen - -**Files:** -- Modify: `public/styles/tasks.css` -- Modify: `public/styles/contacts.css` -- Modify: `public/styles/budget.css` -- Modify: `public/styles/meals.css` - -- [ ] **Step 1: Aktuelle Padding-Werte prüfen** - -```bash -grep -n "padding" public/styles/tasks.css public/styles/contacts.css public/styles/budget.css public/styles/meals.css | grep -v "padding-bottom\|padding-top\|padding-left\|padding-right" | head -30 -``` - -Erwartung: Unterschiedliche Werte (12px, 14px, 16px, 20px). - -- [ ] **Step 2: tasks.css — Task-Card-Padding auf 16px setzen** - -In `public/styles/tasks.css`, alle `.task-card { padding: ... }` Regeln auf `var(--space-4)` vereinheitlichen. Beispiel: - -```css -/* Vorher (ca. Zeile mit .task-card) */ -.task-card { - padding: var(--space-3) var(--space-4); -} - -/* Nachher */ -.task-card { - padding: var(--space-4); -} -``` - -- [ ] **Step 3: contacts.css — Contact-Card-Padding auf 16px** - -In `public/styles/contacts.css`, `.contact-card { padding: ... }` auf `var(--space-4)` setzen. - -- [ ] **Step 4: budget.css — Budget-Entry-Padding auf 16px** - -In `public/styles/budget.css`, `.budget-entry { padding: ... }` auf `var(--space-4)` setzen. - -- [ ] **Step 5: meals.css — Meal-Card-Padding auf 16px** - -In `public/styles/meals.css`, `.meal-card { padding: ... }` auf `var(--space-4)` setzen. - -- [ ] **Step 6: Visuell prüfen** - -```bash -npm run dev -``` - -Browser öffnen → Tasks, Kontakte, Budget, Essensplan. Cards sollen optisch konsistent wirken — gleiche Innenabstände. Nichts soll abgeschnitten oder zu eng wirken. - -- [ ] **Step 7: Commit** - -```bash -git add public/styles/tasks.css public/styles/contacts.css public/styles/budget.css public/styles/meals.css -git commit -m "style: unify card padding to 16px across all modules" -``` - ---- - -### Task 2: FAB-Farben mit Modul-Akzent verknüpfen - -**Files:** -- Modify: `public/styles/layout.css` -- Modify: `public/styles/dashboard.css` -- Modify: `public/styles/tasks.css` -- Modify: `public/styles/shopping.css` -- Modify: `public/styles/meals.css` -- Modify: `public/styles/notes.css` -- Modify: `public/styles/contacts.css` -- Modify: `public/styles/budget.css` - -- [ ] **Step 1: FAB-Basis-Klasse in layout.css prüfen und anpassen** - -```bash -grep -n "fab\|FAB" public/styles/layout.css | head -10 -``` - -In `public/styles/layout.css` die `.fab`-Klasse auf `var(--module-accent, var(--color-btn-primary))` setzen: - -```css -.fab { - background-color: var(--module-accent, var(--color-btn-primary)); - /* alle anderen Eigenschaften bleiben */ -} -.fab:hover { - background-color: color-mix(in srgb, var(--module-accent, var(--color-btn-primary)) 85%, black); -} -``` - -- [ ] **Step 2: Modul-Akzent-Variable pro Modul setzen** - -In jeder Modul-CSS-Datei die `.page`-Klasse (oder root-Selektor) um `--module-accent` ergänzen. - -In `public/styles/dashboard.css`: -```css -.dashboard-page { --module-accent: var(--module-dashboard); } -``` - -In `public/styles/tasks.css`: -```css -.tasks-page { --module-accent: var(--module-tasks); } -``` - -In `public/styles/shopping.css`: -```css -.shopping-page { --module-accent: var(--module-shopping); } -``` - -In `public/styles/meals.css`: -```css -.meals-page { --module-accent: var(--module-meals); } -``` - -In `public/styles/notes.css`: -```css -.notes-page { --module-accent: var(--module-notes); } -``` - -In `public/styles/contacts.css`: -```css -.contacts-page { --module-accent: var(--module-contacts); } -``` - -In `public/styles/budget.css`: -```css -.budget-page { --module-accent: var(--module-budget); } -``` - -- [ ] **Step 3: Prüfen ob Page-Klassen in JS-Modulen gesetzt sind** - -```bash -grep -n "class.*page\|className" public/pages/tasks.js public/pages/shopping.js | head -10 -``` - -Falls die `.tasks-page`/`.shopping-page`-Klasse fehlt, in den jeweiligen `render()`-Funktionen hinzufügen: -```js -container.classList.add('tasks-page'); -``` - -- [ ] **Step 4: Visuell prüfen** - -FAB auf jeder Seite soll in der Akzentfarbe des Moduls erscheinen (Tasks → Grün, Einkauf → Rot-Orange, etc.). - -- [ ] **Step 5: Commit** - -```bash -git add public/styles/layout.css public/styles/dashboard.css public/styles/tasks.css public/styles/shopping.css public/styles/meals.css public/styles/notes.css public/styles/contacts.css public/styles/budget.css -git commit -m "style: tie FAB colors to per-module accent tokens" -``` - ---- - -## Schicht 2 — Animationen & Übergänge - -### Task 3: Seitenübergang auf direktionales Slide-X umstellen - -**Files:** -- Modify: `public/router.js` -- Modify: `public/styles/layout.css` - -- [ ] **Step 1: Aktuelle Transition in layout.css anpassen** - -In `public/styles/layout.css`, die `@keyframes page-in` ersetzen: - -```css -/* Vorher */ -@keyframes page-in { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Nachher */ -@keyframes page-slide-in-right { - from { opacity: 0; transform: translateX(20px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes page-slide-in-left { - from { opacity: 0; transform: translateX(-20px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes page-out-left { - from { opacity: 1; transform: translateX(0); } - to { opacity: 0; transform: translateX(-20px); } -} - -@keyframes page-out-right { - from { opacity: 1; transform: translateX(0); } - to { opacity: 0; transform: translateX(20px); } -} - -.page-transition--in-right { - animation: page-slide-in-right 0.2s var(--ease-out) forwards; -} - -.page-transition--in-left { - animation: page-slide-in-left 0.2s var(--ease-out) forwards; -} - -.page-transition--out-left { - animation: page-out-left 0.12s ease forwards; - pointer-events: none; -} - -.page-transition--out-right { - animation: page-out-right 0.12s ease forwards; - pointer-events: none; -} -``` - -Alte `.page-transition--out` und `@keyframes page-out` entfernen. - -- [ ] **Step 2: router.js — Navigations-Richtung tracken** - -In `public/router.js`, vor der `navigate()`-Funktion: - -```js -// Navigations-Richtung für Slide-Animation -// Reihenfolge der Routen definiert "Tiefe" -const ROUTE_ORDER = ['/', '/tasks', '/shopping', '/meals', '/calendar', - '/notes', '/contacts', '/budget', '/settings']; - -function getDirection(fromPath, toPath) { - const fromIdx = ROUTE_ORDER.indexOf(fromPath ?? '/'); - const toIdx = ROUTE_ORDER.indexOf(toPath); - if (fromIdx === -1 || toIdx === -1 || fromPath === toPath) return 'right'; - return toIdx > fromIdx ? 'right' : 'left'; -} -``` - -- [ ] **Step 3: renderPage() auf direktionale Klassen umstellen** - -In `public/router.js`, die `renderPage()`-Funktion anpassen. Den Block ab `// Alte Seite kurz ausfaden`: - -```js -// Richtung bestimmen -const direction = getDirection(currentPath, route.path); -const outClass = direction === 'right' ? 'page-transition--out-left' : 'page-transition--out-right'; -const inClass = direction === 'right' ? 'page-transition--in-right' : 'page-transition--in-left'; - -// Alte Seite ausfaden -const oldPage = content.querySelector('.page-transition'); -if (oldPage) { - oldPage.classList.add(outClass); - await new Promise(r => setTimeout(r, 120)); -} - -const pageWrapper = document.createElement('div'); -pageWrapper.className = `page-transition ${inClass}`; -content.replaceChildren(pageWrapper); -``` - -Die Zeile `pageWrapper.style.animation = 'page-in 0.2s ease forwards';` entfernen (wird jetzt über Klasse gesteuert). - -**Hinweis:** `currentPath` wird in `navigate()` erst nach der Richtungsberechnung gesetzt. Die Richtungsberechnung muss mit dem alten `currentPath` erfolgen — Zeile `currentPath = path;` erst nach `getDirection()` aufrufen. - -- [ ] **Step 4: prefers-reduced-motion respektieren** - -In `public/styles/layout.css` nach den neuen Keyframes: - -```css -@media (prefers-reduced-motion: reduce) { - .page-transition--in-right, - .page-transition--in-left { - animation: none; - opacity: 1; - } - .page-transition--out-left, - .page-transition--out-right { - animation: none; - } -} -``` - -- [ ] **Step 5: Visuell prüfen** - -Im Browser zwischen mehreren Seiten navigieren. Navigation nach rechts (Dashboard → Aufgaben) soll Slide von rechts zeigen. Browser-Zurück soll von links kommen. Soll flüssig und nicht abrupt sein. - -- [ ] **Step 6: Commit** - -```bash -git add public/router.js public/styles/layout.css -git commit -m "feat: directional slide-x page transitions in router" -``` - ---- - -### Task 4: Shared Utility `stagger()` und Tests - -**Files:** -- Create: `public/utils/ux.js` -- Create: `test-ux-utils.js` - -- [ ] **Step 1: `public/utils/ux.js` erstellen** - -```js -/** - * Modul: UX Utilities - * Zweck: Wiederverwendbare Animationshelfer (Stagger, Vibration) - * Abhängigkeiten: keine - */ - -/** - * Gestaffeltes Einblenden einer NodeList oder eines Arrays von Elementen. - * Maximal MAX_STAGGER Elemente werden verzögert, der Rest sofort eingeblendet. - * - * @param {NodeList|Element[]} elements - * @param {Object} [opts] - * @param {number} [opts.delay=30] — ms zwischen jedem Element - * @param {number} [opts.duration=180] — ms pro Element - * @param {number} [opts.max=5] — Maximale Anzahl gestaffelter Elemente - */ -export function stagger(elements, { delay = 30, duration = 180, max = 5 } = {}) { - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; - const els = Array.from(elements); - els.forEach((el, i) => { - const itemDelay = i < max ? i * delay : max * delay; - el.style.opacity = '0'; - el.style.transform = 'translateY(8px)'; - el.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`; - setTimeout(() => { - el.style.opacity = '1'; - el.style.transform = 'translateY(0)'; - }, itemDelay); - }); -} - -/** - * Vibrationsmuster abspielen, wenn die API verfügbar ist und - * keine reduzierte Bewegung gewünscht wird. - * - * @param {number|number[]} pattern — ms oder [an, aus, an, ...]-Array - */ -export function vibrate(pattern) { - if (!navigator.vibrate) return; - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; - navigator.vibrate(pattern); -} -``` - -- [ ] **Step 2: `test-ux-utils.js` erstellen** - -```js -/** - * Tests: UX Utilities (stagger, vibrate) - * Läuft im Node-Kontext — kein DOM verfügbar, daher nur Pure-Logic-Tests. - */ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; - -// Minimales Window/Navigator-Mock für Node -const { stagger, vibrate } = await (async () => { - // stagger braucht window.matchMedia — wir mocken es - global.window = { - matchMedia: () => ({ matches: false }), - }; - global.navigator = { vibrate: null }; - return import('./public/utils/ux.js'); -})(); - -test('stagger: setzt opacity:0 auf alle Elemente', () => { - const els = [{ style: {} }, { style: {} }, { style: {} }]; - stagger(els, { delay: 0, duration: 0 }); - assert.equal(els[0].style.opacity, '0'); - assert.equal(els[1].style.opacity, '0'); - assert.equal(els[2].style.opacity, '0'); -}); - -test('stagger: tut nichts bei prefers-reduced-motion', () => { - global.window.matchMedia = () => ({ matches: true }); - const els = [{ style: {} }]; - stagger(els); - assert.equal(els[0].style.opacity, undefined); // unverändert - global.window.matchMedia = () => ({ matches: false }); // reset -}); - -test('vibrate: tut nichts wenn API nicht vorhanden', () => { - global.navigator = { vibrate: null }; - assert.doesNotThrow(() => vibrate(10)); -}); - -test('vibrate: ruft navigator.vibrate auf wenn vorhanden', () => { - let called = null; - global.navigator = { vibrate: (p) => { called = p; } }; - vibrate(15); - assert.equal(called, 15); -}); -``` - -- [ ] **Step 3: Test in package.json eintragen** - -In `package.json`, im `"scripts"`-Block: -```json -"test:ux-utils": "node --experimental-vm-modules test-ux-utils.js" -``` - -Und den `"test"`-Script ergänzen: -```json -"test": "... && npm run test:ux-utils" -``` - -- [ ] **Step 4: Tests ausführen** - -```bash -node --experimental-vm-modules test-ux-utils.js -``` - -Erwartung: 4 Tests bestanden, 0 fehlgeschlagen. - -- [ ] **Step 5: Commit** - -```bash -git add public/utils/ux.js test-ux-utils.js package.json -git commit -m "feat: add stagger() and vibrate() UX utilities with tests" -``` - ---- - -### Task 5: Staggered Fade-In in allen Listenmodulen - -**Files:** -- Modify: `public/pages/tasks.js` -- Modify: `public/pages/shopping.js` -- Modify: `public/pages/meals.js` -- Modify: `public/pages/contacts.js` -- Modify: `public/pages/budget.js` -- Modify: `public/pages/notes.js` -- Modify: `public/pages/calendar.js` - -- [ ] **Step 1: Import in tasks.js hinzufügen** - -Am Anfang von `public/pages/tasks.js` nach den bestehenden Imports: - -```js -import { stagger } from '/utils/ux.js'; -``` - -- [ ] **Step 2: stagger() nach Render der Aufgabenliste aufrufen (tasks.js)** - -In der Funktion, die die Aufgabenliste rendert (ca. Zeile 200+), nach dem Einfügen ins DOM: - -```js -// Staggered Fade-In für Listeneinträge -stagger(container.querySelectorAll('.task-item, .kanban-card')); -``` - -- [ ] **Step 3: Import und stagger() in shopping.js** - -```js -import { stagger } from '/utils/ux.js'; -``` - -Nach dem Render der Einkaufsliste: - -```js -stagger(container.querySelectorAll('.shopping-item')); -``` - -- [ ] **Step 4: Import und stagger() in meals.js** - -```js -import { stagger } from '/utils/ux.js'; -``` - -Nach dem Render der Mahlzeiten-Cards: - -```js -stagger(container.querySelectorAll('.meal-card')); -``` - -- [ ] **Step 5: Import und stagger() in contacts.js** - -```js -import { stagger } from '/utils/ux.js'; -``` - -Nach dem Render der Kontaktliste: - -```js -stagger(container.querySelectorAll('.contact-card')); -``` - -- [ ] **Step 6: Import und stagger() in budget.js** - -```js -import { stagger } from '/utils/ux.js'; -``` - -Nach dem Render der Budget-Einträge: - -```js -stagger(container.querySelectorAll('.budget-entry')); -``` - -- [ ] **Step 7: Import und stagger() in notes.js** - -```js -import { stagger } from '/utils/ux.js'; -``` - -Nach dem Render der Notizen-Cards: - -```js -stagger(container.querySelectorAll('.note-card')); -``` - -- [ ] **Step 8: Import und stagger() in calendar.js** - -```js -import { stagger } from '/utils/ux.js'; -``` - -Nach dem Render von Agenda-Einträgen (nur Agenda-Ansicht, nicht Monat/Woche/Tag): - -```js -if (state.view === 'agenda') { - stagger(container.querySelectorAll('.agenda-event')); -} -``` - -- [ ] **Step 9: Visuell prüfen** - -Jede Listenseite öffnen. Einträge sollen beim Laden gestaffelt von unten einblenden (ca. 30ms zwischen jedem, max. 5 gestaffelt). Soll sich lebendig, aber nicht ablenkend anfühlen. - -- [ ] **Step 10: Commit** - -```bash -git add public/pages/tasks.js public/pages/shopping.js public/pages/meals.js public/pages/contacts.js public/pages/budget.js public/pages/notes.js public/pages/calendar.js -git commit -m "feat: staggered fade-in for list items across all modules" -``` - ---- - -### Task 6: Empty States vereinheitlichen (alle Module) - -**Files:** -- Modify: `public/pages/tasks.js` -- Modify: `public/pages/shopping.js` -- Modify: `public/pages/notes.js` -- Modify: `public/pages/meals.js` -- Modify: `public/pages/contacts.js` -- Modify: `public/pages/budget.js` -- Modify: `public/styles/tasks.css` -- Modify: `public/styles/shopping.css` -- Modify: `public/styles/notes.css` -- Modify: `public/styles/contacts.css` -- Modify: `public/styles/budget.css` - -- [ ] **Step 1: tasks.js — `.tasks-empty` auf `.empty-state` umstellen** - -In `public/pages/tasks.js` (ca. Zeile 197), den Block ersetzen: - -```js -// Vorher: -//
-// -//
Keine Aufgaben
-//
...
-//
- -// Nachher: -return ` -
- -
Keine Aufgaben — alles erledigt?
-
Neue Aufgaben über den + Button erstellen.
-
-`; -``` - -- [ ] **Step 2: shopping.js — `.shopping-empty` auf `.empty-state` umstellen** - -In `public/pages/shopping.js` (ca. Zeile 161): - -```js -// Nachher: -`
- -
Die Liste ist leer
-
Artikel über das Eingabefeld oben hinzufügen.
-
` -``` - -- [ ] **Step 3: notes.js — `.notes-empty` auf `.empty-state` umstellen** - -In `public/pages/notes.js` (ca. Zeile 90): - -```js -// Nachher: -`
- -
Noch keine Notizen
-
Neue Notiz über den + Button erstellen.
-
` -``` - -- [ ] **Step 4: meals.js — Empty State ergänzen (fehlte bisher)** - -In `public/pages/meals.js`, in der Funktion die den Tages-Slot rendert, wenn keine Mahlzeit vorhanden: - -```js -// Wenn kein Meal für diesen Slot: Leeren Slot mit Empty-State rendern -if (!meal) { - return ` -
-
-
Kein Essen geplant
-
-
- `; -} -``` - -In `public/styles/layout.css` einen Modifier ergänzen: -```css -.empty-state--compact { - padding: var(--space-4) var(--space-3); - gap: var(--space-2); -} -.empty-state--compact .empty-state__description { - font-size: var(--text-sm); -} -``` - -- [ ] **Step 5: contacts.js — `.contacts-empty` auf `.empty-state` umstellen** - -In `public/pages/contacts.js` (ca. Zeile 136): - -```js -// Nachher — als HTML-String: -`
- -
Noch keine Kontakte
-
Neue Kontakte über den + Button hinzufügen.
-
` -``` - -- [ ] **Step 6: budget.js — `.budget-empty` auf `.empty-state` umstellen** - -In `public/pages/budget.js` (ca. Zeile 242): - -```js -// Nachher: -return ` -
- -
Keine Einträge diesen Monat
-
Budget-Einträge über den + Button hinzufügen.
-
-`; -``` - -- [ ] **Step 7: Modul-eigene Empty-State-CSS-Klassen entfernen** - -In den folgenden CSS-Dateien die modul-eigenen Klassen entfernen (werden durch `.empty-state` aus `layout.css` ersetzt): -- `public/styles/tasks.css`: `.tasks-empty`, `.tasks-empty__icon`, `.tasks-empty__title`, `.tasks-empty__desc` -- `public/styles/shopping.css`: `.shopping-empty`, `.shopping-empty__icon`, `.shopping-empty__title`, `.shopping-empty__desc` -- `public/styles/notes.css`: `.notes-empty`, `.notes-empty__icon` -- `public/styles/contacts.css`: `.contacts-empty` -- `public/styles/budget.css`: `.budget-empty` - -- [ ] **Step 8: Visuell prüfen** - -Alle sechs Module ohne Einträge öffnen. Empty States sollen einheitlich aussehen (zentriert, Icon, Titel, Beschreibung). Meals soll leere Slots kompakt darstellen. - -- [ ] **Step 9: Commit** - -```bash -git add public/pages/tasks.js public/pages/shopping.js public/pages/notes.js public/pages/meals.js public/pages/contacts.js public/pages/budget.js public/styles/tasks.css public/styles/shopping.css public/styles/notes.css public/styles/contacts.css public/styles/budget.css public/styles/layout.css -git commit -m "style: unify all empty states to shared .empty-state class across all modules" -``` - ---- - -### Task 7: Swipe-Reveal proportionale Opacity (Tasks) - -**Files:** -- Modify: `public/pages/tasks.js` - -- [ ] **Step 1: wireSwipeGestures() in tasks.js finden** - -```bash -grep -n "wireSwipeGestures\|swipe-reveal" public/pages/tasks.js | head -15 -``` - -- [ ] **Step 2: Reveal-Opacity proportional zur Swipe-Distanz machen** - -In `public/pages/tasks.js`, in der `wireSwipeGestures()`-Funktion (ca. ab Zeile 680), im `touchmove`-Event-Handler: - -Den Block, der die swipe-reveal-Elemente steuert, ergänzen: - -```js -// Proportionale Opacity des Reveal-Bereichs -const doneReveal = row.querySelector('.swipe-reveal--done'); -const editReveal = row.querySelector('.swipe-reveal--edit'); -const progress = Math.min(Math.abs(deltaX) / SWIPE_THRESHOLD, 1); - -if (deltaX < 0 && doneReveal) { - doneReveal.style.opacity = String(progress); -} else if (deltaX > 0 && editReveal) { - editReveal.style.opacity = String(progress); -} -``` - -Im `touchend`-Handler: Opacity zurücksetzen: - -```js -if (doneReveal) doneReveal.style.opacity = ''; -if (editReveal) editReveal.style.opacity = ''; -``` - -- [ ] **Step 3: Visuell prüfen (mobiler Viewport)** - -Im Browser DevTools auf mobilen Viewport wechseln. Eine Aufgabe nach links wischen. Der grüne Bereich soll proportional zur Wischweite einblenden, nicht abrupt erscheinen. - -- [ ] **Step 4: Commit** - -```bash -git add public/pages/tasks.js -git commit -m "feat: proportional opacity on swipe-reveal action areas" -``` - ---- - -## Schicht 3 — Mobile PWA & Natives Gefühl - -### Task 8: Install-Prompt Timing und Dismiss-Dauer anpassen - -**Files:** -- Modify: `public/components/oikos-install-prompt.js` - -- [ ] **Step 1: Dismiss-Dauer von 30d auf 7d ändern** - -In `public/components/oikos-install-prompt.js`, Zeile 14: - -```js -// Vorher -const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 Tage - -// Nachher -const DISMISS_DURATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage -``` - -- [ ] **Step 2: Interaktions-Zähler einführen** - -Vor der Klassen-Definition: - -```js -const INTERACTION_KEY = 'oikos-install-interactions'; -const INTERACTION_THRESHOLD = 2; // Anzahl Interaktionen vor Prompt -``` - -- [ ] **Step 3: connectedCallback anpassen — erst nach Interaktionen zeigen** - -Die `connectedCallback()`-Methode um Interaktions-Check erweitern: - -```js -connectedCallback() { - if ( - window.matchMedia('(display-mode: standalone)').matches || - navigator.standalone === true - ) return; - - const dismissed = localStorage.getItem(DISMISS_KEY); - if (dismissed && Date.now() - Number(dismissed) < DISMISS_DURATION_MS) return; - - // Noch nicht genug Interaktionen - const interactions = Number(localStorage.getItem(INTERACTION_KEY) || '0'); - if (interactions < INTERACTION_THRESHOLD) { - this._waitForInteractions(); - return; - } - - if (this._isIOS()) { - this._showIOSPrompt(); - } else { - this._listenForInstallPrompt(); - } -} -``` - -- [ ] **Step 4: `_waitForInteractions()` implementieren** - -```js -_waitForInteractions() { - const onInteraction = () => { - const count = Number(localStorage.getItem(INTERACTION_KEY) || '0') + 1; - localStorage.setItem(INTERACTION_KEY, String(count)); - - if (count >= INTERACTION_THRESHOLD) { - document.removeEventListener('click', onInteraction); - if (this._isIOS()) { - this._showIOSPrompt(); - } else { - this._listenForInstallPrompt(); - } - } - }; - document.addEventListener('click', onInteraction); - this._offInteraction = () => document.removeEventListener('click', onInteraction); -} -``` - -- [ ] **Step 5: disconnectedCallback cleanup** - -```js -disconnectedCallback() { - window.removeEventListener('beforeinstallprompt', this._onBeforeInstall); - if (this._offInteraction) this._offInteraction(); -} -``` - -- [ ] **Step 6: Visuell prüfen** - -`localStorage.removeItem('oikos-install-interactions')` in DevTools Console ausführen. App neu laden. Banner soll NICHT sofort erscheinen. Zweimal auf Buttons klicken → Banner soll einblenden. - -- [ ] **Step 7: Commit** - -```bash -git add public/components/oikos-install-prompt.js -git commit -m "feat: delay install prompt until 2 interactions, reduce dismiss to 7 days" -``` - ---- - -### Task 9: Virtual-Keyboard scrollIntoView in modal.js - -**Files:** -- Modify: `public/components/modal.js` - -- [ ] **Step 1: scrollIntoView-Listener in trapFocus() hinzufügen** - -In `public/components/modal.js`, in der `trapFocus()`-Funktion nach dem `container.addEventListener('keydown', ...)`: - -```js -// Virtual Keyboard: Focused Input in sichtbaren Bereich scrollen -function onInputFocus(e) { - const tag = e.target.tagName; - if (tag !== 'INPUT' && tag !== 'TEXTAREA' && tag !== 'SELECT') return; - setTimeout(() => { - e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }, 300); -} -container.addEventListener('focusin', onInputFocus); - -// Cleanup beim Schließen: Referenz speichern -container._onInputFocus = onInputFocus; -``` - -- [ ] **Step 2: Listener in closeModal() entfernen** - -In `closeModal()`, vor `activeOverlay.remove()`: - -```js -const panel = activeOverlay.querySelector('.modal-panel'); -if (panel?._onInputFocus) { - panel.removeEventListener('focusin', panel._onInputFocus); -} -``` - -- [ ] **Step 3: Prüfen im mobilen Viewport** - -DevTools: iPhone-Viewport. Ein Modal öffnen. Auf ein unteres Eingabefeld tippen. Das Feld soll sanft in die Mitte des sichtbaren Bereichs scrollen, wenn die Tastatur erscheint. - -- [ ] **Step 4: Commit** - -```bash -git add public/components/modal.js -git commit -m "feat: scroll focused input into view when virtual keyboard opens" -``` - ---- - -### Task 10: Vibrations-Feedback konsistenzieren - -**Files:** -- Modify: `public/pages/tasks.js` -- Modify: `public/pages/shopping.js` -- Modify: `public/pages/contacts.js` -- Modify: `public/pages/budget.js` -- Modify: `public/pages/notes.js` - -- [ ] **Step 1: Import in allen Modulen ergänzen** - -In jedem der o.g. Module: - -```js -import { vibrate } from '/utils/ux.js'; -``` - -- [ ] **Step 2: tasks.js — bestehende `navigator.vibrate()`-Calls ersetzen** - -```bash -grep -n "navigator.vibrate" public/pages/tasks.js -``` - -Jeden Fund ersetzen: -```js -// Vorher -if (navigator.vibrate) navigator.vibrate(40); - -// Nachher -vibrate(40); -``` - -- [ ] **Step 3: shopping.js — Vibration bei Artikel abhaken** - -In `public/pages/shopping.js`, im Checkbox-Toggle-Handler: - -```js -// Nach erfolgreichem API-Call beim Abhaken: -vibrate(10); -``` - -- [ ] **Step 4: contacts.js — Vibration bei Löschen** - -In `public/pages/contacts.js`, im Delete-Handler (nach API-Aufruf): - -```js -vibrate([30, 50, 30]); -``` - -- [ ] **Step 5: budget.js — Vibration bei Löschen** - -In `public/pages/budget.js`, im Delete-Handler: - -```js -vibrate([30, 50, 30]); -``` - -- [ ] **Step 6: notes.js — Vibration bei Löschen** - -In `public/pages/notes.js`, im Delete-Handler: - -```js -vibrate([30, 50, 30]); -``` - -- [ ] **Step 7: Commit** - -```bash -git add public/pages/tasks.js public/pages/shopping.js public/pages/contacts.js public/pages/budget.js public/pages/notes.js -git commit -m "feat: consistent vibration feedback via vibrate() utility across modules" -``` - ---- - -## Schicht 4 — Formulare & Modals - -### Task 11: Bottom Sheet Modal auf Mobile - -**Files:** -- Modify: `public/components/modal.js` -- Modify: `public/styles/layout.css` - -- [ ] **Step 1: Bottom-Sheet-Styles in layout.css ergänzen** - -In `public/styles/layout.css`, nach den bestehenden Modal-Styles: - -```css -/* ── Bottom Sheet (Mobile < 768px) ── */ -@media (max-width: 767px) { - .modal-overlay { - align-items: flex-end; - } - - .modal-panel { - width: 100%; - max-width: 100%; - max-height: 90dvh; - border-radius: var(--radius-xl) var(--radius-xl) 0 0; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - /* Sheet-Handle */ - padding-top: calc(var(--space-4) + 20px); - animation: sheet-in 0.25s var(--ease-out) forwards; - } - - .modal-panel::before { - content: ''; - position: absolute; - top: var(--space-3); - left: 50%; - transform: translateX(-50%); - width: 36px; - height: 4px; - background: var(--color-border); - border-radius: var(--radius-full); - } - - .modal-panel--closing { - animation: sheet-out 0.2s ease forwards; - } -} - -@keyframes sheet-in { - from { transform: translateY(100%); } - to { transform: translateY(0); } -} - -@keyframes sheet-out { - from { transform: translateY(0); } - to { transform: translateY(100%); } -} - -@media (prefers-reduced-motion: reduce) { - .modal-panel { - animation: none; - } - .modal-panel--closing { - animation: none; - } -} -``` - -- [ ] **Step 2: Animierte Schließung in closeModal() ergänzen** - -In `public/components/modal.js`, die `closeModal()`-Funktion anpassen: - -```js -export function closeModal() { - if (!activeOverlay) return; - - document.removeEventListener('keydown', onEscape); - - if (focusTrapHandler) { - const panel = activeOverlay.querySelector('.modal-panel'); - if (panel) { - panel.removeEventListener('keydown', focusTrapHandler); - if (panel._onInputFocus) panel.removeEventListener('focusin', panel._onInputFocus); - // Sheet-Out-Animation auf Mobile - const isMobile = window.innerWidth < 768; - if (isMobile) { - panel.classList.add('modal-panel--closing'); - panel.addEventListener('animationend', () => { - _doClose(); - }, { once: true }); - return; - } - } - focusTrapHandler = null; - } - - _doClose(); -} - -function _doClose() { - if (!activeOverlay) return; - activeOverlay.remove(); - activeOverlay = null; - document.body.style.overflow = ''; - if (previouslyFocused?.focus) { - previouslyFocused.focus(); - previouslyFocused = null; - } - if (window.oikos?.restoreThemeColor) window.oikos.restoreThemeColor(); -} -``` - -- [ ] **Step 3: Swipe-to-Close für Bottom Sheet** - -In `public/components/modal.js`, in `openModal()` nach `trapFocus(panel)`: - -```js -// Swipe-to-Close auf Mobile -if (window.innerWidth < 768) { - _wireSheetSwipe(panel); -} -``` - -Neue Funktion: - -```js -function _wireSheetSwipe(panel) { - let startY = 0; - let dragging = false; - - panel.addEventListener('touchstart', (e) => { - startY = e.touches[0].clientY; - dragging = true; - }, { passive: true }); - - panel.addEventListener('touchmove', (e) => { - if (!dragging) return; - const dy = e.touches[0].clientY - startY; - if (dy < 0) return; // Kein Swipe nach oben - panel.style.transform = `translateY(${dy * 0.6}px)`; - }, { passive: true }); - - panel.addEventListener('touchend', (e) => { - if (!dragging) return; - dragging = false; - const dy = e.changedTouches[0].clientY - startY; - panel.style.transform = ''; - if (dy > 80) { - closeModal(); - } - }); -} -``` - -- [ ] **Step 4: Visuell prüfen (Mobile Viewport)** - -Modal öffnen. Auf Mobile: Sheet fährt von unten ein, Sheet-Handle sichtbar. Nach unten wischen → Modal schließt sich. - -- [ ] **Step 5: Commit** - -```bash -git add public/components/modal.js public/styles/layout.css -git commit -m "feat: bottom sheet modal on mobile with swipe-to-close" -``` - ---- - -### Task 12: Enter-Tastennavigation zwischen Formularfeldern - -**Files:** -- Modify: `public/components/modal.js` - -- [ ] **Step 1: Enter-Navigation in trapFocus() einbauen** - -In `public/components/modal.js`, in der `trapFocus()`-Funktion, den `focusTrapHandler` erweitern: - -```js -focusTrapHandler = (e) => { - // Tab-Trap (bestehend) - if (e.key === 'Tab') { - const focusable = container.querySelectorAll(FOCUSABLE); - if (!focusable.length) return; - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); first.focus(); - } - } - - // Enter in einzeiligen Inputs → zum nächsten Feld springen - if (e.key === 'Enter') { - const active = document.activeElement; - const isInput = active.tagName === 'INPUT' && active.type !== 'submit' && active.type !== 'button'; - const isSelect = active.tagName === 'SELECT'; - - if (isInput || isSelect) { - const focusable = Array.from(container.querySelectorAll(FOCUSABLE)); - const idx = focusable.indexOf(active); - const next = focusable[idx + 1]; - - if (next && next.tagName !== 'BUTTON') { - e.preventDefault(); - next.focus(); - } - // Beim letzten Feld oder wenn Next ein Button ist: Submit auslösen - if (!next || next.tagName === 'BUTTON') { - const submitBtn = container.querySelector('button[type="submit"], .btn--primary'); - if (submitBtn && !submitBtn.disabled) { - e.preventDefault(); - submitBtn.click(); - } - } - } - } -}; -``` - -- [ ] **Step 2: Prüfen in einem Modal** - -Ein beliebiges Formular öffnen (z.B. neue Aufgabe). Mit Enter durch die Felder navigieren. Letztes Feld + Enter soll den Speichern-Button auslösen. - -- [ ] **Step 3: Commit** - -```bash -git add public/components/modal.js -git commit -m "feat: Enter-key advances focus between form fields and triggers submit on last field" -``` - ---- - -### Task 13: Inline Blur-Validierung in Formularfeldern - -**Files:** -- Modify: `public/styles/layout.css` -- Modify: `public/components/modal.js` - -- [ ] **Step 1: Validierungs-CSS in layout.css ergänzen** - -In `public/styles/layout.css`, nach den Input-Styles: - -```css -/* ── Inline-Validierung ── */ -.form-group { - display: flex; - flex-direction: column; - gap: var(--space-1); -} - -.form-field--error .form-control { - border-color: var(--color-danger); -} - -.form-field--valid .form-control { - border-color: var(--color-success); -} - -.form-field__error { - display: none; - font-size: var(--text-sm); - color: var(--color-danger); - gap: var(--space-1); - align-items: center; -} - -.form-field--error .form-field__error { - display: flex; -} -``` - -- [ ] **Step 2: Blur-Validierung als Utility in modal.js einbauen** - -In `public/components/modal.js`, neue Hilfsfunktion: - -```js -/** - * Aktiviert Blur-Validierung für alle required-Inputs in einem Container. - * Aufgerufen von onSave-Callbacks der einzelnen Seiten-Module. - * - * @param {HTMLElement} formContainer - */ -export function wireBlurValidation(formContainer) { - formContainer.querySelectorAll('input[required], select[required], textarea[required]').forEach((input) => { - input.addEventListener('blur', () => { - const group = input.closest('.form-field') ?? input.parentElement; - const hasValue = input.value.trim().length > 0; - group?.classList.toggle('form-field--error', !hasValue); - group?.classList.toggle('form-field--valid', hasValue); - }); - }); -} -``` - -- [ ] **Step 3: Fehleranzeige-HTML-Struktur in tasks.js einbauen (als Beispiel)** - -In `public/pages/tasks.js`, im Formular-HTML der Aufgaben-Modal, Titelfeld anpassen: - -```html - - - - - -
- - -
- - Dieses Feld ist erforderlich. -
-
-``` - -- [ ] **Step 4: wireBlurValidation() im onSave-Callback von tasks.js aufrufen** - -In `public/pages/tasks.js`, im `openModal()`-Aufruf, innerhalb von `onSave`: - -```js -import { openModal, closeModal, wireBlurValidation } from '/components/modal.js'; - -// ... -onSave: (panel) => { - wireBlurValidation(panel); - // ... bestehender Event-Binding-Code -} -``` - -- [ ] **Step 5: Visuell prüfen** - -Neue Aufgabe → Titel-Feld anklicken → wieder verlassen ohne Eingabe → roter Rand + Fehlermeldung. Etwas eingeben → grüner Rand. - -- [ ] **Step 6: Commit** - -```bash -git add public/components/modal.js public/styles/layout.css public/pages/tasks.js -git commit -m "feat: blur-triggered inline validation for required form fields" -``` - ---- - -### Task 14: Submit-Feedback (Checkmark-Erfolg, Shake-Fehler) - -**Files:** -- Modify: `public/styles/layout.css` -- Modify: `public/components/modal.js` - -- [ ] **Step 1: Animationen in layout.css ergänzen** - -In `public/styles/layout.css`: - -```css -/* ── Submit-Feedback Animationen ── */ -@keyframes btn-shake { - 0%, 100% { transform: translateX(0); } - 20% { transform: translateX(-4px); } - 40% { transform: translateX(4px); } - 60% { transform: translateX(-4px); } - 80% { transform: translateX(4px); } -} - -.btn--shaking { - animation: btn-shake 0.3s ease; -} - -.btn--success { - background-color: var(--color-success) !important; - color: #fff !important; - pointer-events: none; -} -``` - -- [ ] **Step 2: Submit-Feedback Utility in modal.js** - -In `public/components/modal.js`, neue exportierte Funktionen: - -```js -/** - * Zeigt Erfolgs-Feedback auf einem Button (Checkmark für 600ms). - * @param {HTMLButtonElement} btn - * @param {string} [originalLabel] - */ -export function btnSuccess(btn, originalLabel) { - const label = originalLabel ?? btn.textContent; - btn.classList.add('btn--success'); - btn.innerHTML = ` - - `; - setTimeout(() => { - btn.classList.remove('btn--success'); - btn.textContent = label; - }, 700); -} - -/** - * Zeigt Fehler-Feedback auf einem Button (Shake-Animation). - * @param {HTMLButtonElement} btn - */ -export function btnError(btn) { - btn.classList.remove('btn--shaking'); - // Reflow erzwingen damit Animation neu startet - void btn.offsetWidth; - btn.classList.add('btn--shaking'); - btn.addEventListener('animationend', () => btn.classList.remove('btn--shaking'), { once: true }); -} -``` - -- [ ] **Step 3: btnSuccess/btnError in tasks.js verwenden** - -In `public/pages/tasks.js`, Import erweitern: - -```js -import { openModal, closeModal, wireBlurValidation, btnSuccess, btnError } from '/components/modal.js'; -``` - -Im Form-Submit-Handler der Aufgaben: - -```js -// Nach erfolgreichem API-Call: -btnSuccess(saveBtn, 'Speichern'); -setTimeout(() => closeModal(), 700); - -// Bei API-Fehler: -btnError(saveBtn); -showToast('Fehler beim Speichern.', 'danger'); -``` - -- [ ] **Step 4: Visuell prüfen** - -Neue Aufgabe erstellen → Speichern → Button soll kurz grün mit Checkmark werden → Modal schließt sich. Absichtlich Netzwerkfehler simulieren (DevTools → Offline) → Button soll schütteln. - -- [ ] **Step 5: Commit** - -```bash -git add public/styles/layout.css public/components/modal.js public/pages/tasks.js -git commit -m "feat: checkmark success and shake error feedback on form submit buttons" -``` - ---- - -## Abschluss - -- [ ] **Alle Tests ausführen** - -```bash -npm test -``` - -Erwartung: Alle bisherigen Tests + neue `test-ux-utils.js`-Tests grün. - -- [ ] **Vollständige Durchsicht im Browser (Mobile + Desktop)** - -Jede Seite öffnen und prüfen: -- Cards haben einheitliche Abstände ✓ -- FAB-Farben entsprechen dem Modul ✓ -- Seitenübergänge sind direktional (links/rechts) ✓ -- Listen blenden gestaffelt ein ✓ -- Empty States sind einheitlich ✓ -- Swipe-Reveal in Aufgaben blendet proportional ein ✓ -- Install-Prompt erscheint erst nach 2 Interaktionen ✓ -- Virtuelles Keyboard scrollt Inputs in den sichtbaren Bereich ✓ -- Vibration bei Aktionen (Mobile) ✓ -- Modals öffnen als Bottom Sheet auf Mobile ✓ -- Swipe-to-Close funktioniert ✓ -- Enter navigiert durch Formularfelder ✓ -- Blur-Validierung zeigt Fehler unmittelbar ✓ -- Submit-Button gibt Erfolg/Fehler-Feedback ✓ - -- [ ] **CHANGELOG.md aktualisieren** - -Im `[Unreleased]`-Block: - -```markdown -### Changed -- Directional slide-x page transitions (forward = right, backward = left) -- PWA install prompt delayed until 2 user interactions, dismiss window reduced to 7 days -- Unified card padding to 16px across all modules - -### Added -- Staggered fade-in animation for list items on page load -- Unified empty states using shared `.empty-state` class in contacts and budget -- Proportional opacity on swipe-reveal action areas (tasks) -- scrollIntoView for focused inputs when virtual keyboard opens -- Consistent vibration feedback via `vibrate()` utility across modules -- Bottom sheet modal on mobile (< 768px) with swipe-to-close and sheet handle -- Enter-key navigation between form fields; Enter on last field triggers submit -- Blur-triggered inline validation for required fields with error/success states -- Submit button checkmark-success (700ms) and shake-error feedback animations -- FAB colors tied to per-module accent tokens -``` - -- [ ] **Final Commit** - -```bash -git add CHANGELOG.md -git commit -m "docs: update CHANGELOG for UX polish (all 4 layers)" -``` diff --git a/docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md b/docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md deleted file mode 100644 index 4bc423f..0000000 --- a/docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md +++ /dev/null @@ -1,476 +0,0 @@ -# Shopping Swipe Gestures Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add left-swipe-to-toggle and right-swipe-to-delete touch gestures to Shopping list items on mobile, replacing the visible × delete button on small screens. - -**Architecture:** Shared swipe base CSS is moved from `tasks.css` to `layout.css` so both modules can use it. Shopping-specific styles (delete reveal, mobile button hiding) go in `shopping.css`. `shopping.js` wraps each item in a `swipe-row` and registers touch handlers via a new `wireSwipeGestures()` function, called after every list re-render. - -**Tech Stack:** Vanilla JS (ES modules), Touch Events API, CSS custom properties, Node.js built-in test runner (regression tests only — no DOM test framework in this project). - ---- - -## File Map - -| File | Change | -|------|--------| -| `public/styles/tasks.css` | Remove `.swipe-row`, `.swipe-reveal`, `.swipe-reveal--done` (moved to layout.css) | -| `public/styles/layout.css` | Add moved shared swipe styles + `.swipe-reveal--done` | -| `public/styles/shopping.css` | Add `.swipe-reveal--delete`, `.swipe-row .shopping-item`, `.swipe-row--swiping .shopping-item`, mobile hide for `.item-delete` | -| `public/pages/shopping.js` | Wrap `renderItem()` output in swipe-row, add `wireSwipeGestures(container)`, call it from `updateItemsList()` | - ---- - -### Task 1: Move shared swipe CSS from tasks.css to layout.css - -**Files:** -- Modify: `public/styles/tasks.css` (lines 170–229) -- Modify: `public/styles/layout.css` (append before Print section) - -- [ ] **Step 1: Remove base swipe styles from tasks.css** - -In `public/styles/tasks.css`, delete the block from `/* Swipe-Wrapper (Mobil-Gesten) */` through `.swipe-reveal--done { ... }` (lines 170–216). Keep only the task-specific rules that follow: - -```css -/* Kein Margin mehr am Task-Card selbst (übernimmt swipe-row) */ -.swipe-row .task-card { - margin-bottom: 0; - border-radius: var(--radius-md); - position: relative; - z-index: 1; - will-change: transform; -} - -/* Rechts hinter der Karte = Bearbeiten (Swipe nach rechts) */ -.swipe-reveal--edit { - left: 0; - background-color: var(--color-accent); - color: #fff; - border-radius: var(--radius-md) 0 0 var(--radius-md); -} - -/* Touch-Feedback: leichte Hervorhebung während Swipe */ -.swipe-row--swiping .task-card { - box-shadow: var(--shadow-lg); -} -``` - -Add a comment header so the section remains clear: - -```css -/* -------------------------------------------------------- - * Swipe-Wrapper — Task-spezifische Styles - * Basis-Styles (.swipe-row, .swipe-reveal, .swipe-reveal--done) - * liegen in layout.css - * -------------------------------------------------------- */ -``` - -- [ ] **Step 2: Add shared swipe styles to layout.css** - -In `public/styles/layout.css`, find the Print section (`/* Print-Styles */`) and insert the following block **directly before it**: - -```css -/* -------------------------------------------------------- - * Swipe-Wrapper — Gemeinsame Basis (Tasks + Shopping) - * Modul-spezifische Styles (.swipe-reveal--edit, .swipe-reveal--delete, - * .swipe-row .task-card, .swipe-row .shopping-item) liegen in den Modul-CSS. - * -------------------------------------------------------- */ -.swipe-row { - position: relative; - overflow: hidden; - border-radius: var(--radius-md); - margin-bottom: var(--space-2); - /* Verhindert ungewolltes Flackern auf iOS */ - -webkit-backface-visibility: hidden; -} - -/* Reveal-Panels hinter der Karte */ -.swipe-reveal { - position: absolute; - top: 0; - bottom: 0; - width: 50%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-1); - font-size: var(--text-xs); - font-weight: var(--font-weight-semibold); - opacity: 0; - pointer-events: none; - z-index: 0; - transition: opacity 0.05s linear; -} - -/* Gemeinsam: Erledigt / Abhaken (Swipe nach links) */ -.swipe-reveal--done { - right: 0; - background-color: var(--color-success); - color: #fff; - border-radius: 0 var(--radius-md) var(--radius-md) 0; -} -``` - -- [ ] **Step 3: Run regression tests** - -```bash -npm test -``` - -Expected: `# pass 12` and `# fail 0` - -- [ ] **Step 4: Verify tasks swipe still works visually** - -Open the app in a browser (or mobile DevTools touch emulation), go to Tasks list, swipe an item left and right. The green "Erledigt" and blue "Bearbeiten" panels must still appear correctly. - -- [ ] **Step 5: Commit** - -```bash -git add public/styles/tasks.css public/styles/layout.css -git commit -m "refactor: move shared swipe CSS from tasks.css to layout.css" -``` - ---- - -### Task 2: Add shopping-specific swipe CSS - -**Files:** -- Modify: `public/styles/shopping.css` - -- [ ] **Step 1: Find the correct insertion point in shopping.css** - -In `public/styles/shopping.css`, locate the section for `.shopping-item` styles. The new swipe styles go **after** the existing `.shopping-item` block. - -- [ ] **Step 2: Add the styles** - -Append the following after the existing `.shopping-item` rules in `public/styles/shopping.css`: - -```css -/* -------------------------------------------------------- - * Swipe-Wrapper — Shopping-spezifische Styles - * -------------------------------------------------------- */ - -/* Kein Margin mehr am shopping-item selbst (übernimmt swipe-row) */ -.swipe-row .shopping-item { - margin-bottom: 0; - border-radius: var(--radius-md); - position: relative; - z-index: 1; - will-change: transform; -} - -/* Rechts hinter der Karte = Löschen (Swipe nach rechts) */ -.swipe-reveal--delete { - left: 0; - background-color: var(--color-danger); - color: #fff; - border-radius: var(--radius-md) 0 0 var(--radius-md); -} - -/* Touch-Feedback: leichte Hervorhebung während Swipe */ -.swipe-row--swiping .shopping-item { - box-shadow: var(--shadow-lg); -} - -/* × Löschen-Button auf Mobile ausblenden — Swipe übernimmt */ -@media (max-width: 1023px) { - .item-delete { - display: none; - } -} -``` - -- [ ] **Step 3: Run regression tests** - -```bash -npm test -``` - -Expected: `# pass 12` and `# fail 0` - -- [ ] **Step 4: Commit** - -```bash -git add public/styles/shopping.css -git commit -m "feat: add shopping swipe CSS (delete reveal, mobile button hide)" -``` - ---- - -### Task 3: Wrap renderItem with swipe-row in shopping.js - -**Files:** -- Modify: `public/pages/shopping.js` - -- [ ] **Step 1: Update renderItem()** - -Find `function renderItem(item)` in `public/pages/shopping.js` (currently returns a `.shopping-item` div). Replace the entire function with: - -```js -function renderItem(item) { - const isDone = Boolean(item.is_checked); - return ` -
- - -
- -
-
${item.name}
- ${item.quantity ? `
${item.quantity}
` : ''} -
- -
-
`; -} -``` - -- [ ] **Step 2: Run regression tests** - -```bash -npm test -``` - -Expected: `# pass 12` and `# fail 0` - -- [ ] **Step 3: Verify shopping list renders without errors** - -Open the app in a browser, navigate to Shopping. Items must render with the swipe-row wrapper. The × button must be hidden on mobile (visible on desktop ≥ 1024px). The existing checkbox toggle must still work. - -- [ ] **Step 4: Commit** - -```bash -git add public/pages/shopping.js -git commit -m "feat: wrap shopping items in swipe-row" -``` - ---- - -### Task 4: Add wireSwipeGestures and wire into updateItemsList - -**Files:** -- Modify: `public/pages/shopping.js` - -- [ ] **Step 1: Add constants at the top of shopping.js** - -After the `import` statements (before the `state` declaration), add: - -```js -// Swipe-Gesten Konstanten (identisch zu tasks.js) -const SWIPE_THRESHOLD = 80; // px — Mindestweg für Aktion -const SWIPE_MAX_VERT = 12; // px — vertikaler Toleranzbereich -const SWIPE_LOCK_VERT = 30; // px — ab diesem Weg gilt es als Scroll -``` - -- [ ] **Step 2: Add wireSwipeGestures() function** - -Add the following function **before** `updateItemsList()` in `shopping.js`: - -```js -function wireSwipeGestures(container) { - const listEl = container.querySelector('#items-list'); - if (!listEl) return; - - listEl.querySelectorAll('.swipe-row').forEach((row) => { - let startX = 0, startY = 0; - let dx = 0; - let locked = false; // false | 'swipe' | 'scroll' - const card = row.querySelector('.shopping-item'); - if (!card) return; - - function resetCard(animate = true) { - card.style.transition = animate ? 'transform 0.25s ease' : ''; - card.style.transform = ''; - row.classList.remove('swipe-row--swiping'); - row.querySelector('.swipe-reveal--done').style.opacity = '0'; - row.querySelector('.swipe-reveal--delete').style.opacity = '0'; - } - - row.addEventListener('touchstart', (e) => { - if (document.getElementById('shared-modal-overlay')) return; - startX = e.touches[0].clientX; - startY = e.touches[0].clientY; - dx = 0; - locked = false; - card.style.transition = ''; - }, { passive: true }); - - row.addEventListener('touchmove', (e) => { - if (locked === 'scroll') return; - - const currentX = e.touches[0].clientX; - const currentY = e.touches[0].clientY; - dx = currentX - startX; - const dy = Math.abs(currentY - startY); - - if (locked === false) { - if (dy > SWIPE_MAX_VERT && Math.abs(dx) < dy) { - locked = 'scroll'; - resetCard(false); - return; - } - if (Math.abs(dx) > SWIPE_MAX_VERT) { - locked = 'swipe'; - } - } - - if (locked !== 'swipe') return; - - if (dy < SWIPE_LOCK_VERT) e.preventDefault(); - - const dampened = dx > 0 - ? Math.min(dx, SWIPE_THRESHOLD + (dx - SWIPE_THRESHOLD) * 0.2) - : Math.max(dx, -(SWIPE_THRESHOLD + (-dx - SWIPE_THRESHOLD) * 0.2)); - - card.style.transform = `translateX(${dampened}px)`; - row.classList.add('swipe-row--swiping'); - - const progress = Math.min(Math.abs(dx) / SWIPE_THRESHOLD, 1); - if (dx < 0) { - row.querySelector('.swipe-reveal--done').style.opacity = String(progress); - row.querySelector('.swipe-reveal--delete').style.opacity = '0'; - } else { - row.querySelector('.swipe-reveal--delete').style.opacity = String(progress); - row.querySelector('.swipe-reveal--done').style.opacity = '0'; - } - }, { passive: false }); - - row.addEventListener('touchend', async () => { - if (locked !== 'swipe') { resetCard(false); return; } - - const itemId = Number(row.dataset.swipeId); - const checked = Number(row.dataset.swipeChecked); - - if (dx < -SWIPE_THRESHOLD) { - // Swipe links → abhaken / zurück - card.style.transition = 'transform 0.2s ease'; - card.style.transform = 'translateX(-110%)'; - vibrate(40); - setTimeout(async () => { - resetCard(false); - const newVal = checked ? 0 : 1; - const item = state.items.find((i) => i.id === itemId); - if (item) { - item.is_checked = newVal; - updateItemsList(container); - updateListCounter(state.activeListId, 0, newVal ? 1 : -1); - renderTabs(container); - } - try { - await api.patch(`/shopping/items/${itemId}`, { is_checked: newVal }); - vibrate(10); - } catch (err) { - if (item) item.is_checked = checked; - updateItemsList(container); - window.oikos.showToast(err.message, 'danger'); - } - }, 200); - - } else if (dx > SWIPE_THRESHOLD) { - // Swipe rechts → löschen - card.style.transition = 'transform 0.2s ease'; - card.style.transform = 'translateX(110%)'; - vibrate(40); - setTimeout(async () => { - const item = state.items.find((i) => i.id === itemId); - try { - await api.delete(`/shopping/items/${itemId}`); - state.items = state.items.filter((i) => i.id !== itemId); - updateItemsList(container); - updateListCounter(state.activeListId, -1, item?.is_checked ? -1 : 0); - renderTabs(container); - } catch (err) { - resetCard(true); - window.oikos.showToast(err.message, 'danger'); - } - }, 200); - - } else { - resetCard(true); - } - }); - }); -} -``` - -- [ ] **Step 3: Call wireSwipeGestures from updateItemsList()** - -In `updateItemsList(container)`, after the `stagger(...)` call, add: - -```js -wireSwipeGestures(container); -``` - -The updated block looks like this: - -```js -function updateItemsList(container) { - const listEl = container.querySelector('#items-list'); - if (listEl) { - listEl.innerHTML = renderItems(); - if (window.lucide) window.lucide.createIcons(); - stagger(listEl.querySelectorAll('.shopping-item')); - wireSwipeGestures(container); // ← new - } - // ... rest of function unchanged -} -``` - -- [ ] **Step 4: Run regression tests** - -```bash -npm test -``` - -Expected: `# pass 12` and `# fail 0` - -- [ ] **Step 5: Verify swipe gestures work end-to-end** - -Test in mobile DevTools (Chrome → Toggle device toolbar → touch emulation): - -1. Open Shopping, add 3 test items -2. Swipe an item **left** past 80 px → green panel appears → item toggles to checked (strikethrough) → swipe again to uncheck -3. Swipe an item **right** past 80 px → red panel appears → item is removed from list -4. Swipe < 80 px in either direction → card springs back, no action -5. Scroll the list vertically → no swipe triggers -6. On desktop (≥ 1024px): × delete button is visible, swipe not triggered by mouse - -- [ ] **Step 6: Update CHANGELOG.md** - -Add to `## [Unreleased]` → `### Added`: - -``` -- Shopping: swipe-left to toggle checked/unchecked, swipe-right to delete items on mobile; × delete button hidden on mobile in favour of swipe gesture -``` - -- [ ] **Step 7: Commit** - -```bash -git add public/pages/shopping.js CHANGELOG.md -git commit -m "feat: swipe gestures on shopping list items (toggle + delete)" -``` - ---- - -### Task 5: Push - -- [ ] **Push to remote** - -```bash -git push -``` diff --git a/docs/superpowers/specs/2026-03-30-ux-polish-design.md b/docs/superpowers/specs/2026-03-30-ux-polish-design.md deleted file mode 100644 index b349919..0000000 --- a/docs/superpowers/specs/2026-03-30-ux-polish-design.md +++ /dev/null @@ -1,224 +0,0 @@ -# UX Polish — Design-Spezifikation - -**Datum:** 2026-03-30 -**Status:** Genehmigt -**Scope:** UX-Verbesserungen (Phase 1 vor Featureerweiterungen) - ---- - -## Ausgangslage - -Oikos v0.1.0 ist funktional vollständig und alle 146 Tests sind grün. Das UI wirkt jedoch steril und beliebig — es fehlt eine eigene Persönlichkeit. Zusätzlich gibt es Konsistenzlücken zwischen Modulen, abrupte Übergänge, ein suboptimales mobiles Erlebnis und verbesserungswürdige Formular-UX. - -Die Verbesserungen erfolgen in vier aufeinander aufbauenden Schichten (Layer for Layer), sodass jede Schicht das Fundament der nächsten bildet. - ---- - -## Schicht 1 — Design-Sprache & Konsistenz - -### Ziel -Der App eine eigene, wiedererkennbare Persönlichkeit geben — klar und präzise als Hauptrichtung, mit einem Hauch Wärme und Familiarität. - -### Typografie-Skala -Vier klar unterscheidbare Stufen ersetzen die aktuelle flache Hierarchie: - -| Stufe | Größe | Gewicht | Einsatz | -|-------|-------|---------|---------| -| Display | 24px | 700 | Seitentitel, Modal-Titel | -| Title | 18px | 600 | Widget-Überschriften, Gruppen-Header | -| Body | 15px | 400 | Fließtext, Listeneinträge | -| Caption | 13px | 400 | Metadaten, Zeitstempel, Labels | - -Überschriften (Display, Title) erhalten `letter-spacing: -0.3px` für einen modernen, knappen Look. - -### Farb-Tokens -Die Grautöne erhalten einen minimalen Warmton-Shift, um den Charakter von "Tech-App" zu "Familien-App" zu verschieben: - -```css -/* Vorher → Nachher */ ---color-bg: #F5F5F7 → #F6F5F3 /* ganz leicht warmer Tint */ ---color-surface: #FFFFFF → #FFFFFF /* bleibt rein */ ---color-text-primary:#1C1C1E → #1A1A1F /* minimal wärmer */ -``` - -Der Accent-Blau (`#007AFF`) bleibt unverändert. Dunkel-Modus erhält analoge Anpassungen. - -### Komponenten-Konsistenz -Alle Module erhalten identische Card-Tokens: -- Padding: `16px` überall (aktuell variiert zwischen 12px–20px je Modul) -- Schatten: `shadow-sm` im Ruhezustand, `shadow-md` bei Hover -- Buttons: `:hover` = leichter Helligkeitsshift, `:active` = `scale(0.97)` (haptisches Feedback-Gefühl) - -### Modul-Akzentfarben -Jedes Modul erhält eine dezente, eigene Akzentfarbe für Page-Header und FAB. Die Farben sind bereits in der Architektur vorgesehen (theme-color), werden aber vervollständigt und konsequent eingesetzt: - -| Modul | Akzent | -|-------|--------| -| Dashboard | `#007AFF` (Standard-Blau) | -| Aufgaben | `#FF9500` (Orange) | -| Einkauf | `#34C759` (Grün) | -| Essensplan | `#FF6B35` (Warm-Orange) | -| Kalender | `#5AC8FA` (Hellblau) | -| Notizen | `#FFCC00` (Gelb) | -| Kontakte | `#AF52DE` (Violett) | -| Budget | `#30B0C7` (Teal) | - ---- - -## Schicht 2 — Animationen & Übergänge - -### Ziel -Die App fühlt sich lebendig an. Alle Animationen respektieren `prefers-reduced-motion: reduce`. - -### Seitenübergänge -Neue Seite fährt von rechts ein, alte geht nach links raus: -- Transform: `translateX(24px) → translateX(0)` -- Opacity: `0 → 1` -- Dauer: `200ms`, Easing: `ease-out` -- Zurück-Navigation: gespiegelte Richtung - -Implementierung im zentralen `router.js` — kein Modul-Code nötig. - -### Gestaffelte Listen-Einblendung (Staggered Fade-In) -Beim initialen Laden einer Seite erscheinen Listenelemente und Cards nacheinander: -- Jedes Item: `opacity 0 → 1` + `translateY(8px) → 0` -- Verzögerung: 30ms pro Item, maximal 5 Items gestaffelt (danach sofort) -- Dauer pro Item: `180ms` - -### Micro-Interactions - -**Checkbox (Aufgaben erledigt):** -Das SVG-Häkchen zeichnet sich per `stroke-dashoffset`-Animation ein (60ms). Die Karten-Zeile bekommt einen `text-decoration: line-through`-Transition (100ms). - -**FAB:** -`scale(0.92)` beim `:active` + Ripple-Effekt (radial expandierender Kreis, 300ms, opacity 0→1→0). - -**Swipe-Reveal (Aufgaben):** -Aktuell erscheint der farbige Hintergrund abrupt. Neu: Hintergrundfarbe und Icon blenden proportional zur Swipe-Distanz ein (`opacity: swipeDistance / SWIPE_THRESHOLD`). - -### Skeleton-Loading -Dashboard-Widgets zeigen beim Laden animierte Skeleton-Platzhalter: -- Shimmer-Animation via `@keyframes` (linearer Gradient läuft durch, 1.4s, unendlich) -- Schematische Rechtecke in Card-Form, passend zur jeweiligen Widget-Größe -- Ersetzt leere Flächen während des API-Calls - -### Empty States -Jede leere Liste erhält einen Inline-SVG-Platzhalter und einen kontextuellen CTA: - -| Modul | Text | CTA | -|-------|------|-----| -| Aufgaben | "Keine Aufgaben — alles erledigt?" | "+ Aufgabe erstellen" | -| Einkauf | "Die Liste ist leer" | "+ Artikel hinzufügen" | -| Essensplan | "Kein Essen geplant" | "Mahlzeit eintragen" | -| Notizen | "Noch keine Notizen" | "+ Notiz erstellen" | -| Kontakte | "Noch keine Kontakte" | "+ Kontakt hinzufügen" | -| Budget | "Keine Buchungen diesen Monat" | "+ Buchung eintragen" | - -SVGs sind kleine, themenbezogene Illustrationen (Linien-Icons, kein Clipart), inline im HTML, kein externer Fetch. - ---- - -## Schicht 3 — Mobile PWA & Natives Gefühl - -### Ziel -Die installierte App fühlt sich auf dem Handy nativ an. - -### PWA-Install-Prompt -**Timing:** Prompt erscheint erst nach 2–3 Benutzerinteraktionen (z.B. nach dem ersten erfolgreich erstellten Eintrag), nicht sofort beim ersten Seitenaufruf. - -**Darstellung:** Bottom Sheet von unten einfahrend (nicht abruptes Banner). Enthält App-Icon, Name "Oikos", kurzen Nutzentext. - -**Wiederholung:** Einmal abgelehnt → 7 Tage nicht erneut zeigen (via `localStorage` mit Timestamp). - -**Plattformspezifisch:** -- Android: natives `beforeinstallprompt`-Event -- iOS: eigene Anleitung ("Teilen → Zum Home-Bildschirm") da kein natives Event - -### Scroll & Overscroll -Auf allen scrollbaren Containern: -- `overscroll-behavior: contain` — verhindert Browser-Pull-to-Refresh innerhalb der App -- `-webkit-overflow-scrolling: touch` — Momentum-Scrolling auf iOS -- Bottom Nav und Header: `position: sticky` mit `env(safe-area-inset-bottom)` — kein Layout-Shift durch dynamische Viewport-Höhe (iOS Safari) - -### Vibrations-Feedback -`navigator.vibrate()` bei bedeutsamen Aktionen, nur wenn API verfügbar und `prefers-reduced-motion` nicht gesetzt: - -| Aktion | Muster | -|--------|--------| -| Aufgabe erledigt | `10ms` | -| Swipe-Aktion ausgelöst | `15ms` | -| Eintrag gelöscht | `[30, 50, 30]ms` | -| Fehlermeldung | `[20, 40, 20]ms` | - -### Keyboard-Verhalten (Virtuelles Keyboard) -Beim Tippen in ein Eingabefeld springt dieses automatisch in den sichtbaren Bereich: -```js -input.addEventListener('focus', () => { - setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300); -}); -``` -300ms Verzögerung gibt dem Keyboard Zeit, sich zu öffnen. - -### Theme-Color -Dynamische `theme-color` Meta-Tag-Aktualisierung beim Modulwechsel wird vervollständigt — jedes Modul übergibt beim Rendern seine Akzentfarbe, die Browser-Chrome-Farbe wechselt entsprechend. - ---- - -## Schicht 4 — Formulare & Modals - -### Ziel -Eingaben sind schnell, klar und fehlertolerant — besonders auf Mobil. - -### Auto-Fokus & Tastaturnavigation -- Beim Öffnen eines Modals: Fokus springt automatisch auf erstes Eingabefeld (`setTimeout(0)` nach Modal-Render) -- `Tab`: logische Feldreihenfolge (entspricht DOM-Reihenfolge) -- `Enter` in einzeiligen Inputs: springt zum nächsten Feld -- `Enter` im letzten Feld (oder Textarea + `Ctrl+Enter`): löst Submit aus -- `Escape`: schließt Modal - -### Inline-Validierung -- Trigger: `blur`-Event auf jedem Feld (nicht erst bei Submit) -- Fehlermeldung: direkt unter dem Feld, `color: var(--color-danger)`, mit Warn-Icon -- Erfolgreiche Pflichtfelder: dezenter grüner Rand (`border-color: var(--color-success)`) -- Submit-Button: deaktiviert solange Pflichtfelder leer, aktiv sobald Minimalanforderungen erfüllt - -### Modal-UX auf Mobil -Auf Screens < 768px werden Modals als **Bottom Sheet** dargestellt: -- Einfähranimation: `translateY(100%) → translateY(0)`, 250ms, `ease-out` -- Maximalhöhe: `90dvh`, intern scrollbar -- Swipe-to-Close: Swipe nach unten > 80px schließt Modal; zwischen 0–80px gibt es gummibandartigen Widerstand (`transform: translateY(distance * 0.4)`) -- Backdrop-Klick: schließt Modal -- Schließanimation: `translateY(0) → translateY(100%)`, 200ms - -Auf Desktop (≥ 768px): zentriertes Modal bleibt unverändert (Backdrop-Klick + Escape schließen). - -### Submit-Feedback -**Erfolg:** -1. Submit-Button: Label wird durch Checkmark-Icon ersetzt (600ms) -2. Modal schließt sich mit Slide-Down-Animation -3. Liste aktualisiert sich (optimistisch oder via Re-Fetch) - -**Fehler:** -1. Submit-Button: `shake`-Animation (300ms, ±4px horizontal) -2. Fehlermeldung erscheint unter dem betreffenden Feld oder als Banner oben im Modal -3. Kein Datenverlust — alle eingegebenen Werte bleiben erhalten - ---- - -## Nicht in Scope - -- Neue Features (Meal Drag&Drop, Budget-Recurrence, Kalender-Auto-Sync) — diese kommen erst nach UX + Code-Qualität -- Backend-Änderungen — alle vier Schichten sind rein frontend-seitig -- Push-Benachrichtigungen — explizit v1.1 (BACKLOG) -- Grundlegende Architekturänderungen am Router oder API-Layer - ---- - -## Reihenfolge der Implementierung - -1. Schicht 1: `tokens.css`, `reset.css`, `layout.css`, alle Modul-CSS-Dateien -2. Schicht 2: `router.js` (Seitenübergänge), alle Page-Module (Staggering, Micro-Interactions), `dashboard.js` (Skeleton) -3. Schicht 3: `oikos-install-prompt.js`, `sw.js`, alle Page-Module (Scroll, Keyboard, Vibration) -4. Schicht 4: `components/modal.js`, alle Page-Module (Formulare, Validierung) - -Jede Schicht ist ein eigener Commit-Block und kann unabhängig reviewt werden. diff --git a/docs/superpowers/specs/2026-03-31-module-accent-colors-design.md b/docs/superpowers/specs/2026-03-31-module-accent-colors-design.md deleted file mode 100644 index e6dc844..0000000 --- a/docs/superpowers/specs/2026-03-31-module-accent-colors-design.md +++ /dev/null @@ -1,73 +0,0 @@ -# Modul-Akzentfarben stärker nutzen — Design Spec - -**Date:** 2026-03-31 -**Status:** Approved - -## Ziel - -Die vorhandenen `--module-*` CSS-Tokens (Dashboard=Blau, Tasks=Grün, Kalender=Violett, Mahlzeiten=Orange, Einkauf=Rot-Orange, Notizen=Gold, Kontakte=Kräftiges Blau, Budget=Teal) werden aktuell nur für die FAB-Hintergrundfarbe genutzt. Ziel: drei weitere visuelle Ebenen mit Modul-Akzenten versehen, damit der Nutzer sofort erkennt, in welchem Modul er sich befindet. - -## Drei Änderungsbereiche - -### A — Aktiver Tab in Navigation (Bottom-Nav + Sidebar) - -**Problem:** `--module-accent` ist auf dem Page-Wrapper gesetzt (z. B. `.tasks-page`), aber die Nav-Bar liegt im App-Shell außerhalb dieser Wrapper. CSS-Kaskade funktioniert nicht. - -**Lösung — JS:** In `updateNav(path)` (router.js) wird nach dem Setzen von `aria-current` zusätzlich `--active-module-accent` als CSS Custom Property auf `document.documentElement` geschrieben: - -```js -const module = ROUTES.find(r => r.path === path)?.module; -const accent = module ? getCSSToken(`--module-${module}`) : ''; -document.documentElement.style.setProperty('--active-module-accent', accent || ''); -``` - -**Lösung — CSS** (layout.css): Alle drei Stellen, die aktuell `var(--color-accent)` für den aktiven Nav-State nutzen, werden auf `var(--active-module-accent, var(--color-accent))` umgestellt: - -1. `.nav-item[aria-current="page"] { color: ... }` — Bottom-Nav Icon + Label -2. `.nav-sidebar .nav-item[aria-current="page"] { color: ...; background-color: ... }` — Sidebar Highlight -3. `.nav-sidebar .nav-item[aria-current="page"]::before { background: ... }` — Sidebar linker Akzentstreifen - -Das Fallback `var(--color-accent)` stellt sicher, dass Login-Screen und Fehlerseiten ohne Modul-Kontext korrekt dargestellt werden. - ---- - -### B — Seitenkopf-Streifen (3px border-top) - -`border-top: 3px solid var(--module-accent)` wird auf den Toolbar/Header-Selektor jedes Moduls gesetzt. Da diese Elemente innerhalb des Page-Wrappers liegen, erben sie `--module-accent` direkt. - -| Modul | Selektor | CSS-Datei | -|-------|----------|-----------| -| Tasks | `.tasks-toolbar` | tasks.css | -| Notizen | `.notes-toolbar` | notes.css | -| Kontakte | `.contacts-toolbar` | contacts.css | -| Kalender | `.cal-toolbar` | calendar.css | -| Einkauf | `.list-header` | shopping.css | -| Budget | `.budget-list-header` | budget.css | -| Mahlzeiten | — | entfällt (kein Toolbar) | -| Dashboard | — | entfällt (Widget-Grid, kein Toolbar) | - ---- - -### C — Karten-Randstreifen (3px border-left) - -`border-left: 3px solid var(--module-accent)` auf den Hauptkarten-/Zeilen-Elementen. Der linke `border-radius` wird auf `0` gesetzt damit der Streifen sauber anliegt (`border-radius: 0 var(--radius-md) var(--radius-md) 0`). - -| Modul | Selektor | CSS-Datei | Bemerkung | -|-------|----------|-----------|-----------| -| Tasks | `.task-card` | tasks.css | | -| Einkauf | `.shopping-item` | shopping.css | | -| Kontakte | `.contact-item` | contacts.css | | -| Budget | `.budget-entry` | budget.css | Hat bereits Einnahmen/Ausgaben-Dot — kein Konflikt | -| Kalender | ❌ | — | Eigene Event-Farblogik | -| Notizen | ❌ | — | Eigene Karten-Hintergrundfarben | -| Mahlzeiten | ❌ | — | Slot-Layout, keine klassische Liste | -| Dashboard | ❌ | — | Widget-Struktur | - ---- - -## Out of Scope - -- Kein Umbau des Farbsystems oder der Token-Namen -- Kein Dark-Mode-spezifisches Anpassen (Dark-Mode-Tokens für `--module-*` existieren bereits in tokens.css) -- Keine neuen Modul-Farben -- Keine Änderungen an Meals, Dashboard, Calendar-Events, Notes-Karten diff --git a/docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md b/docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md deleted file mode 100644 index 1e1acbd..0000000 --- a/docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md +++ /dev/null @@ -1,79 +0,0 @@ -# Shopping Swipe Gestures — Design Spec - -**Date:** 2026-03-31 -**Status:** Approved - -## Scope - -Add swipe gestures to Shopping list items on mobile. Notes and other modules are explicitly out of scope. - -## Behaviour - -| Gesture | Action | Reveal colour | -|---------|--------|---------------| -| Swipe left (> threshold) | Toggle checked/unchecked | Green (`--color-success`) | -| Swipe right (> threshold) | Delete item | Red (`--color-danger`) | - -- Reveal label for left swipe: "Abhaken" (unchecked) or "Zurück" (already checked) -- Reveal label for right swipe: "Löschen" (always) -- Threshold, damping, and scroll-lock logic identical to `tasks.js` -- On swipe-right delete: optimistic DOM removal → `DELETE /api/v1/shopping/items/:id` → on error restore item and show danger toast -- On swipe-left toggle: optimistic DOM update (class toggle) → `PATCH /api/v1/shopping/items/:id` → on error revert and show danger toast - -## CSS Changes - -**`layout.css`** — receives shared swipe infrastructure (moved from `tasks.css`): -- `.swipe-row` base styles -- `.swipe-reveal` base styles -- `.swipe-reveal--done` (green, used by tasks and shopping) - -**`tasks.css`** — retains only task-specific styles: -- `.swipe-row .task-card` -- `.swipe-reveal--edit` (blue, tasks only) -- `.swipe-row--swiping .task-card` - -**`shopping.css`** — new shopping-specific styles: -- `.swipe-row .shopping-item` -- `.swipe-row--swiping .shopping-item` -- `.swipe-reveal--delete` (red, `--color-danger`) -- `@media (max-width: 1023px) .item-delete { display: none }` — × button hidden on mobile, swipe replaces it - -## JavaScript Changes (`shopping.js`) - -### `renderItem(item)` → wrapped in swipe-row - -```html -
-
- - Abhaken|Zurück -
-
- - Löschen -
- -
-``` - -### New `wireSwipeGestures(container)` - -Registers `touchstart` / `touchmove` (passive: false) / `touchend` on each `.swipe-row` inside `#items-list`. Logic mirrors tasks.js: - -1. `touchstart`: record `startX`, `startY`, clear `locked` flag -2. `touchmove`: determine swipe vs. vertical scroll via angle; once locked to swipe, translate card, fade-in appropriate reveal panel proportionally -3. `touchend`: if locked and `|dx| > SWIPE_THRESHOLD` trigger action; otherwise spring back - -Constants (same as tasks.js): -- `SWIPE_THRESHOLD = 80` px -- `SWIPE_LOCK_VERT = 8` px -- `SWIPE_MAX_VERT = 10` px - -Called from `renderContent()` after DOM update, alongside existing `wireAutocomplete` and `wireQuickAdd` calls. Also called from `rerenderItems()` after any state change that re-renders the list. - -## Out of Scope - -- Notes swipe gestures -- Any other module -- Undo toast after delete (delete is immediate; existing × button provided undo-less delete already) -- Desktop swipe via mouse/pointer events