feat: add flexible reminder options for birthdays
Add support for customizable birthday reminders with preset offsets (none, at time, 15min, 1h, 1d, 2d, 1w, 2w) and custom intervals. Users can now configure when to be reminded of upcoming birthdays. - Add migration 31: reminder_offset, reminder_custom_amount, reminder_custom_unit to birthdays table - Update POST/PUT /birthdays routes to accept reminder fields - Add getOffsetMinutes() helper in birthday service - Update birthdayReminderAt() to calculate reminder time with offset - Modify syncBirthdayReminder() to handle empty offset (no reminder) - Add renderBirthdayReminderSection() UI component - Move reminder-custom CSS from calendar.css to reminders.css - Add protocol check to service worker (non-http protocol guard) All translations already present in de.json. Tests: 109 passing, 0 failing. Co-Authored-By: Rafael Foster <rafaelfoster@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Oikos UX/UI Analyse</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #0f0f0e;
|
||||
color: #f0ede8;
|
||||
min-height: 100vh;
|
||||
padding: 32px 24px 80px;
|
||||
}
|
||||
.header { max-width: 920px; margin: 0 auto 40px; }
|
||||
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: #818CF8; margin-bottom: 8px; }
|
||||
h1 { font-size: clamp(26px, 4vw, 40px); font-weight: 700; letter-spacing: -.03em; line-height: 1.1; margin-bottom: 10px; }
|
||||
h1 span { color: #818CF8; }
|
||||
.sub { font-size: 14px; color: #8E8D89; line-height: 1.5; }
|
||||
|
||||
.summary { max-width: 920px; margin: 0 auto 40px; background: rgba(129,140,248,.08); border: 1px solid rgba(129,140,248,.2); border-radius: 16px; padding: 24px; display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; text-align: center; }
|
||||
@media(max-width:480px){ .summary { grid-template-columns: 1fr; } }
|
||||
.sum-num { font-size: 38px; font-weight: 700; letter-spacing: -.03em; line-height: 1; }
|
||||
.sum-lbl { font-size: 11px; color: #8E8D89; margin-top: 4px; }
|
||||
.c { color: #FCA5A5; } .h { color: #FCD34D; } .m { color: #6EE7B7; }
|
||||
|
||||
hr { max-width: 920px; margin: 0 auto 36px; border: none; border-top: 1px solid rgba(255,255,255,.06); }
|
||||
.section { max-width: 920px; margin: 0 auto 40px; }
|
||||
.slbl { font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||||
.slbl::after { content:''; flex:1; height:1px; background:rgba(255,255,255,.06); }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px,1fr)); gap: 14px; }
|
||||
.card { background: #1a1a18; border-radius: 14px; border: 1px solid rgba(255,255,255,.06); padding: 18px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.card:hover { border-color: rgba(255,255,255,.12); }
|
||||
.card-ico { width: 34px; height: 34px; border-radius: 9px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; margin-bottom: 2px; }
|
||||
.ico-c { background: rgba(252,165,165,.12); }
|
||||
.ico-h { background: rgba(252,211,77,.1); }
|
||||
.ico-m { background: rgba(110,231,183,.1); }
|
||||
.card-title { font-size: 13px; font-weight: 600; line-height: 1.35; }
|
||||
.card-body { font-size: 12px; color: #8E8D89; line-height: 1.55; }
|
||||
.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 99px; font-size: 10px; font-weight: 600; margin-top: auto; align-self: flex-start; }
|
||||
.bc { color:#FCA5A5; background:rgba(252,165,165,.1); }
|
||||
.bh { color:#FCD34D; background:rgba(252,211,77,.08); }
|
||||
.bm { color:#6EE7B7; background:rgba(110,231,183,.08); }
|
||||
|
||||
.strengths { display: grid; grid-template-columns: repeat(auto-fill,minmax(200px,1fr)); gap: 10px; }
|
||||
.str { background: rgba(129,140,248,.06); border: 1px solid rgba(129,140,248,.12); border-radius: 12px; padding: 14px; }
|
||||
.str-title { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
|
||||
.str-body { font-size: 11px; color: #8E8D89; line-height: 1.5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="eyebrow">Oikos UX/UI Analyse · April 2026</div>
|
||||
<h1>Was funktioniert — was bremst <span>Oikos</span></h1>
|
||||
<p class="sub">70 % Mobile-PWA · 30 % Desktop · 11 Module · Vanilla JS</p>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<div><div class="sum-num c">4</div><div class="sum-lbl">Kritisch<br>sofort beheben</div></div>
|
||||
<div><div class="sum-num h">5</div><div class="sum-lbl">Hohe Priorität<br>nächster Sprint</div></div>
|
||||
<div><div class="sum-num m">4</div><div class="sum-lbl">Mittlere Priorität<br>Backlog</div></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section">
|
||||
<div class="slbl" style="color:#FCA5A5">🔴 Kritisch</div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-ico ico-c">🧭</div>
|
||||
<div class="card-title">Sidebar 1024–1279 px: Icons ohne Labels/Tooltips</div>
|
||||
<div class="card-body">Bei häufigster Desktop-Auflösung sind Modul-Icons ohne jede Beschriftung — kein Tooltip, kein Label. 11 Module sind nicht erkennbar.</div>
|
||||
<div class="badge bc">Keine Discoverability</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-c">👆</div>
|
||||
<div class="card-title">Modal-Close: 40 px statt 44 px Minimum</div>
|
||||
<div class="card-body"><code>.modal-panel__close</code> nutzt <code>--target-md</code> (40 px). iOS-Minimum ist 44 pt. Auf kleinen Screens spürbares Frustrationspotenzial.</div>
|
||||
<div class="badge bc">Apple HIG Violation</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-c">🔗</div>
|
||||
<div class="card-title">Widget-Links: Tap-Target < 44 px</div>
|
||||
<div class="card-body"><code>.widget__link</code>: 12 px Text, kein <code>min-height</code>. Effektiver Tippbereich ~16–18 px. Auf jedem Dashboard-Widget vorhanden.</div>
|
||||
<div class="badge bc">Tap-Target Violation</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-c">🔁</div>
|
||||
<div class="card-title">Doppelter FAB: .fab vs .page-fab</div>
|
||||
<div class="card-body">Zwei nahezu identische FAB-Klassen mit unterschiedlicher <code>bottom</code>-Berechnung erzeugen inkonsistente Positionierung auf verschiedenen Seiten.</div>
|
||||
<div class="badge bc">Inkonsistente UI</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="slbl" style="color:#FCD34D">🟡 Hoch</div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-ico ico-h">🗂️</div>
|
||||
<div class="card-title">Onboarding: 3 Screens für 11 Module</div>
|
||||
<div class="card-body">3 generische Steps erklären nicht Budget, RRule-Wiederholungen oder Calendar-Sync. Kein Feature-Discovery danach.</div>
|
||||
<div class="badge bh">Discoverability</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-h">📊</div>
|
||||
<div class="card-title">Widget-Reihenfolge nicht anpassbar</div>
|
||||
<div class="card-body">Config speichert nur <code>{ id, visible }</code> — kein order-Feld. Familien mit verschiedenen Prioritäten können das Dashboard nicht sinnvoll anpassen.</div>
|
||||
<div class="badge bh">Personalisierung</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-h">↩️</div>
|
||||
<div class="card-title">Inkonsistentes Undo</div>
|
||||
<div class="card-body">Manche Aktionen zeigen Undo-Toast, andere nicht. Kein zentrales Undo-System. Bei Kontakt/Notiz löschen fehlt der Weg zurück.</div>
|
||||
<div class="badge bh">Fehlererholung</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-h">📡</div>
|
||||
<div class="card-title">Kein sichtbarer Offline-Indikator</div>
|
||||
<div class="card-body">Service Worker existiert, aber kein Offline-Banner in der App-Shell. Nutzer merken Offline erst beim API-Fehler — zu spät.</div>
|
||||
<div class="badge bh">PWA-Erfahrung</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-h">⌨️</div>
|
||||
<div class="card-title">Keine Keyboard Shortcuts</div>
|
||||
<div class="card-body">30 % Desktop-Nutzung ohne globale Shortcuts. Kein „N" für neu, kein „/" für Suche. Power-User auf Maus angewiesen.</div>
|
||||
<div class="badge bh">Desktop-Ergonomie</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="slbl" style="color:#6EE7B7">🟢 Mittel</div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-ico ico-m">📦</div>
|
||||
<div class="card-title">reminders.css global geladen</div>
|
||||
<div class="card-body">Wird auf allen Seiten geparst, nicht lazy. Kein Blocking-Problem, aber vermeidbare CSS-Last auf Seiten ohne Reminder-UI.</div>
|
||||
<div class="badge bm">Performance</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-m">👉</div>
|
||||
<div class="card-title">Swipe nur Tasks & Shopping</div>
|
||||
<div class="card-body">Swipe-Reveal fehlt bei Kontakten, Notizen, Geburtstagen. Inkonsistente Erwartungshaltung im selben App-Kontext.</div>
|
||||
<div class="badge bm">Interaktions-Konsistenz</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-m">🎨</div>
|
||||
<div class="card-title">11 Modulfarben gleichzeitig im Dashboard</div>
|
||||
<div class="card-body">Wenn alle Widgets sichtbar sind, treffen 11 Akzentfarben aufeinander. Das Dashboard kann farblich überladen wirken.</div>
|
||||
<div class="badge bm">Visuelle Ruhe</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-ico ico-m">💬</div>
|
||||
<div class="card-title">Toast: kein Swipe-to-Dismiss</div>
|
||||
<div class="card-body">Toasts sind nicht wegwischbar. Auf iOS/Android ist Swipe-to-Dismiss eine fest etablierte Konvention — ihr Fehlen fällt auf.</div>
|
||||
<div class="badge bm">Mobile-Konvention</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section">
|
||||
<div class="slbl" style="color:#818CF8">✨ Was bereits ausgezeichnet ist</div>
|
||||
<div class="strengths">
|
||||
<div class="str"><div class="str-title">🌙 Dark Mode Architektur</div><div class="str-body">Private/Public-Token-Pattern ist mustergültig. Alle Kontraste WCAG-geprüft.</div></div>
|
||||
<div class="str"><div class="str-title">♿ 4 A11y-Schichten</div><div class="str-body">reduced-motion, reduced-transparency, prefers-contrast, forced-colors — alle implementiert.</div></div>
|
||||
<div class="str"><div class="str-title">🍎 iOS-PWA-Bewusstsein</div><div class="str-body">100dvh + webkit-Fallback, safe-area-inset, Flex-Kind statt fixed nav.</div></div>
|
||||
<div class="str"><div class="str-title">💎 Glass Design System</div><div class="str-body">@supports-basiert, opake Fallbacks für reduced-transparency, konsistente Token.</div></div>
|
||||
<div class="str"><div class="str-title">🎭 Modul-Theming</div><div class="str-body">--active-module-accent System elegant — FAB, Nav, Toggles spiegeln aktive Seite.</div></div>
|
||||
<div class="str"><div class="str-title">📐 Design Tokens</div><div class="str-body">Vollständige Skala, kaum Hardcoding. Basis für alles weitere.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,629 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Oikos — Lösungsvorschläge</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #0f0f0e; color: #f0ede8;
|
||||
min-height: 100vh; padding: 32px 24px 100px;
|
||||
}
|
||||
code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: .9em; background: rgba(255,255,255,.08); padding: 1px 5px; border-radius: 4px; }
|
||||
a { color: #818CF8; text-decoration: none; }
|
||||
|
||||
.page-title { max-width: 960px; margin: 0 auto 48px; }
|
||||
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing:.1em; text-transform:uppercase; color:#818CF8; margin-bottom:8px; }
|
||||
h1 { font-size: clamp(24px,4vw,38px); font-weight:700; letter-spacing:-.03em; line-height:1.1; margin-bottom:10px; }
|
||||
h1 span { color:#818CF8; }
|
||||
.sub { font-size:14px; color:#8E8D89; line-height:1.5; }
|
||||
|
||||
/* Fix block */
|
||||
.fix { max-width: 960px; margin: 0 auto 56px; }
|
||||
.fix__header { display:flex; align-items:flex-start; gap:16px; margin-bottom:24px; }
|
||||
.fix__num { width:40px; height:40px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:16px; font-weight:700; flex-shrink:0; margin-top:2px; }
|
||||
.num-c { background:rgba(252,165,165,.15); color:#FCA5A5; }
|
||||
.num-h { background:rgba(252,211,77,.12); color:#FCD34D; }
|
||||
.num-m { background:rgba(110,231,183,.1); color:#6EE7B7; }
|
||||
.fix__meta { flex:1; }
|
||||
.fix__tag { font-size:10px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-bottom:4px; }
|
||||
.fix__title { font-size:20px; font-weight:700; letter-spacing:-.02em; line-height:1.2; }
|
||||
.fix__why { font-size:13px; color:#8E8D89; line-height:1.6; margin-top:6px; }
|
||||
|
||||
/* Split layout */
|
||||
.split { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
|
||||
@media(max-width:640px){ .split { grid-template-columns:1fr; } }
|
||||
|
||||
.panel { background:#1a1a18; border-radius:14px; border:1px solid rgba(255,255,255,.06); overflow:hidden; }
|
||||
.panel__head { padding:10px 16px; font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; border-bottom:1px solid rgba(255,255,255,.06); display:flex; align-items:center; gap:8px; }
|
||||
.dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
||||
.dot-r { background:#FCA5A5; } .dot-g { background:#6EE7B7; }
|
||||
.panel__body { padding:20px; }
|
||||
|
||||
/* Code panel */
|
||||
.code-panel { background:#111110; border-radius:14px; border:1px solid rgba(255,255,255,.06); overflow:hidden; }
|
||||
.code-panel__head { padding:10px 16px; font-size:11px; font-weight:600; color:#8E8D89; border-bottom:1px solid rgba(255,255,255,.06); display:flex; align-items:center; justify-content:space-between; }
|
||||
.code-panel__body { padding:16px 20px; font-family:'SF Mono','Fira Code',monospace; font-size:12px; line-height:1.7; overflow-x:auto; }
|
||||
.del { color:#FCA5A5; } .add { color:#6EE7B7; } .cmt { color:#636360; }
|
||||
|
||||
/* Sidebar mock */
|
||||
.mock-sidebar { display:flex; flex-direction:column; gap:4px; width:60px; background:#222220; border-radius:10px; padding:10px 6px; }
|
||||
.mock-sidebar-wide { width:190px; }
|
||||
.mock-si { display:flex; align-items:center; gap:10px; padding:8px; border-radius:8px; color:#8E8D89; font-size:12px; position:relative; }
|
||||
.mock-si--active { background:rgba(129,140,248,.15); color:#818CF8; }
|
||||
.mock-si__ico { width:18px; height:18px; background:currentColor; border-radius:4px; opacity:.5; flex-shrink:0; }
|
||||
.mock-si--active .mock-si__ico { opacity:1; }
|
||||
.mock-tooltip { position:absolute; left:calc(100% + 10px); top:50%; transform:translateY(-50%); background:#2A2A28; border:1px solid rgba(255,255,255,.12); color:#f0ede8; font-size:11px; padding:4px 10px; border-radius:6px; white-space:nowrap; z-index:10; pointer-events:none; }
|
||||
.mock-tooltip::before { content:''; position:absolute; right:100%; top:50%; transform:translateY(-50%); border:5px solid transparent; border-right-color:#2A2A28; }
|
||||
|
||||
/* Touch mock */
|
||||
.touch-compare { display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap; }
|
||||
.touch-item { display:flex; flex-direction:column; align-items:center; gap:8px; }
|
||||
.touch-box { border-radius:10px; border:1.5px solid; display:flex; align-items:center; justify-content:center; font-weight:600; font-size:13px; }
|
||||
.tb-bad { width:40px; height:40px; border-color:rgba(252,165,165,.4); background:rgba(252,165,165,.06); color:#FCA5A5; }
|
||||
.tb-good { width:44px; height:44px; border-color:rgba(110,231,183,.4); background:rgba(110,231,183,.06); color:#6EE7B7; }
|
||||
.touch-lbl { font-size:11px; color:#8E8D89; text-align:center; }
|
||||
.touch-lbl strong { color:#f0ede8; display:block; }
|
||||
|
||||
/* Widget link mock */
|
||||
.widget-mock { background:#222220; border-radius:10px; padding:12px 14px; }
|
||||
.wm-row { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
|
||||
.wm-title { font-size:13px; font-weight:600; }
|
||||
.wm-link { font-size:11px; }
|
||||
.wm-link--bad { color:#818CF8; padding:2px 0; } /* no min-height */
|
||||
.wm-link--good { color:#818CF8; padding:8px 10px; background:rgba(129,140,248,.1); border-radius:6px; min-height:32px; display:flex; align-items:center; }
|
||||
.wm-body { height:40px; background:rgba(255,255,255,.04); border-radius:6px; }
|
||||
|
||||
/* FAB comparison */
|
||||
.fab-compare { display:flex; flex-direction:column; gap:12px; }
|
||||
.fab-row { display:flex; align-items:center; gap:12px; padding:10px 12px; background:rgba(255,255,255,.04); border-radius:8px; border:1px solid rgba(255,255,255,.06); }
|
||||
.fab-circle { width:44px; height:44px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:20px; flex-shrink:0; }
|
||||
.fab-info { flex:1; }
|
||||
.fab-name { font-size:12px; font-weight:600; font-family:monospace; }
|
||||
.fab-desc { font-size:11px; color:#8E8D89; margin-top:2px; }
|
||||
.fab-bad { background:rgba(252,165,165,.12); border:1.5px solid rgba(252,165,165,.3); }
|
||||
.fab-good { background:rgba(110,231,183,.1); border:1.5px solid rgba(110,231,183,.25); }
|
||||
|
||||
/* Onboarding mock */
|
||||
.ob-flow { display:flex; gap:12px; overflow-x:auto; padding-bottom:4px; }
|
||||
.ob-card { background:#222220; border-radius:12px; padding:16px; width:140px; flex-shrink:0; text-align:center; border:1px solid rgba(255,255,255,.06); }
|
||||
.ob-card--highlight { border-color:rgba(129,140,248,.3); background:rgba(129,140,248,.06); }
|
||||
.ob-ico { width:36px; height:36px; border-radius:10px; background:rgba(129,140,248,.15); margin:0 auto 8px; display:flex; align-items:center; justify-content:center; font-size:18px; }
|
||||
.ob-title { font-size:11px; font-weight:600; margin-bottom:4px; }
|
||||
.ob-desc { font-size:10px; color:#8E8D89; line-height:1.4; }
|
||||
.ob-dots { display:flex; justify-content:center; gap:4px; margin-top:8px; }
|
||||
.ob-dot { width:6px; height:6px; border-radius:50%; background:rgba(255,255,255,.15); }
|
||||
.ob-dot--on { background:#818CF8; width:16px; border-radius:3px; }
|
||||
|
||||
/* Widget order mock */
|
||||
.widget-order { display:flex; flex-direction:column; gap:8px; }
|
||||
.wo-item { display:flex; align-items:center; gap:10px; padding:10px 12px; background:#222220; border-radius:8px; border:1px solid rgba(255,255,255,.06); cursor:grab; }
|
||||
.wo-handle { color:#636360; font-size:14px; }
|
||||
.wo-dot { width:10px; height:10px; border-radius:3px; flex-shrink:0; }
|
||||
.wo-label { font-size:13px; font-weight:500; flex:1; }
|
||||
.wo-vis { font-size:10px; color:#8E8D89; }
|
||||
|
||||
/* Offline banner mock */
|
||||
.offline-banner { padding:10px 16px; background:rgba(161,98,7,.2); border:1px solid rgba(161,98,7,.3); border-radius:10px; display:flex; align-items:center; gap:10px; margin-bottom:10px; }
|
||||
.ob-icon { font-size:16px; }
|
||||
.ob-text { font-size:12px; color:#FCD34D; flex:1; }
|
||||
.ob-pill { font-size:10px; font-weight:600; padding:2px 8px; background:rgba(161,98,7,.25); color:#FCD34D; border-radius:99px; }
|
||||
|
||||
/* Keyboard shortcut mock */
|
||||
.kbd-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:8px; }
|
||||
.kbd-row { display:flex; align-items:center; gap:10px; padding:8px 12px; background:#1a1a18; border-radius:8px; border:1px solid rgba(255,255,255,.06); }
|
||||
.kbd { padding:2px 7px; background:#2A2A28; border:1px solid rgba(255,255,255,.15); border-bottom:2px solid rgba(255,255,255,.08); border-radius:5px; font-size:11px; font-family:monospace; color:#f0ede8; }
|
||||
.kbd-label { font-size:12px; color:#8E8D89; }
|
||||
|
||||
/* Divider */
|
||||
hr { max-width:960px; margin:0 auto 48px; border:none; border-top:1px solid rgba(255,255,255,.06); }
|
||||
|
||||
/* Undo */
|
||||
.undo-arch { display:flex; flex-direction:column; gap:8px; }
|
||||
.undo-row { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#1a1a18; border-radius:8px; border:1px solid rgba(255,255,255,.06); }
|
||||
.undo-action { font-size:12px; font-weight:600; flex:1; }
|
||||
.undo-badge { font-size:10px; padding:2px 8px; border-radius:99px; }
|
||||
.ub-yes { color:#6EE7B7; background:rgba(110,231,183,.1); }
|
||||
.ub-no { color:#FCA5A5; background:rgba(252,165,165,.1); }
|
||||
.ub-new { color:#FCD34D; background:rgba(252,211,77,.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-title">
|
||||
<div class="eyebrow">Lösungsvorschläge · Oikos · April 2026</div>
|
||||
<h1>Konkrete <span>Optimierungen</span> — sortiert nach Impact</h1>
|
||||
<p class="sub">13 Punkte · 4 kritisch · 5 hoch · 4 mittel</p>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════ KRITISCH ════════════════════════════════ -->
|
||||
|
||||
<!-- FIX 1: Sidebar Tooltips -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-c">1</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCA5A5">Kritisch · 1–2 h</div>
|
||||
<div class="fix__title">Sidebar: title-Tooltips für icon-only Modus</div>
|
||||
<div class="fix__why">Nutzer bei 1024–1279 px sehen 11 Icons ohne jede Beschriftung. Ein einfaches <code>title</code>-Attribut auf jedem Nav-Item genügt als sofortiger Fix. Langfristig: expanded sidebar ab 1280 px früher aktivieren.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-r"></span> Ist-Zustand</div>
|
||||
<div class="panel__body" style="display:flex;justify-content:center">
|
||||
<div class="mock-sidebar">
|
||||
<div class="mock-si mock-si--active"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#FCA5A5;margin-left:16px;align-self:center">Kein Hinweis<br>was die Icons bedeuten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Soll-Zustand</div>
|
||||
<div class="panel__body" style="display:flex;justify-content:center">
|
||||
<div class="mock-sidebar" style="position:relative;overflow:visible">
|
||||
<div class="mock-si mock-si--active" style="position:relative">
|
||||
<div class="mock-si__ico"></div>
|
||||
<div class="mock-tooltip">Dashboard</div>
|
||||
</div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
<div class="mock-si"><div class="mock-si__ico"></div></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#6EE7B7;margin-left:24px;align-self:center">Tooltip bei<br>Hover/Focus sichtbar</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-panel" style="margin-top:14px">
|
||||
<div class="code-panel__head"><span>router.js — Nav-Item Rendering</span><span style="color:#6EE7B7">+1 Zeile</span></div>
|
||||
<div class="code-panel__body">
|
||||
<span class="cmt">// In renderAppShell() oder wo nav-items erzeugt werden:</span>
|
||||
<span class="del">- a.setAttribute('aria-label', label);</span>
|
||||
<span class="add">+ a.setAttribute('aria-label', label);</span>
|
||||
<span class="add">+ a.setAttribute('title', label); <span class="cmt">// Tooltip für collapsed sidebar</span></span>
|
||||
<br>
|
||||
<span class="cmt">/* Optional: CSS-Tooltip statt native title (für Styling) */</span>
|
||||
<span class="add">+ .nav-sidebar .nav-item[title]:hover::after {</span>
|
||||
<span class="add">+ content: attr(title);</span>
|
||||
<span class="add">+ position: absolute;</span>
|
||||
<span class="add">+ left: calc(100% + 10px);</span>
|
||||
<span class="add">+ /* ... Tooltip-Styles */</span>
|
||||
<span class="add">+ }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 2: Modal Close Button -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-c">2</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCA5A5">Kritisch · 5 min</div>
|
||||
<div class="fix__title">Modal-Close: von 40 px auf 44 px</div>
|
||||
<div class="fix__why">Ein Einzeiler in <code>layout.css</code>. Der Fehler liegt darin, dass <code>--target-md</code> (40 px) statt <code>--target-base</code> (44 px) verwendet wird. Das Token existiert bereits — es muss nur gewechselt werden.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-r"></span> Problem: 40 × 40 px</div>
|
||||
<div class="panel__body">
|
||||
<div class="touch-compare">
|
||||
<div class="touch-item">
|
||||
<div class="touch-box tb-bad">✕</div>
|
||||
<div class="touch-lbl"><strong>40 × 40 px</strong>–target-md<br>4 px unter Minimum</div>
|
||||
</div>
|
||||
<div style="flex:1;font-size:12px;color:#8E8D89;line-height:1.6;align-self:center">
|
||||
Ein durchschnittlicher Fingertipp belegt 44–50 px. Beim Schließen unter Stress (mit einer Hand, unterwegs) erhöht sich die Fehlklickrate spürbar.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Fix: 44 × 44 px</div>
|
||||
<div class="panel__body">
|
||||
<div class="touch-compare">
|
||||
<div class="touch-item">
|
||||
<div class="touch-box tb-good">✕</div>
|
||||
<div class="touch-lbl"><strong>44 × 44 px</strong>--target-base<br>Apple HIG ✓</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-panel" style="margin-top:14px">
|
||||
<div class="code-panel__head"><span>layout.css · .modal-panel__close</span><span style="color:#6EE7B7">1 Zeile</span></div>
|
||||
<div class="code-panel__body">
|
||||
<span class="del">- width: var(--target-md); /* 40px */</span>
|
||||
<span class="del">- height: var(--target-md); /* 40px */</span>
|
||||
<span class="add">+ width: var(--target-base); /* 44px — Apple HIG Minimum */</span>
|
||||
<span class="add">+ height: var(--target-base); /* 44px */</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 3: Widget Links -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-c">3</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCA5A5">Kritisch · 15 min</div>
|
||||
<div class="fix__title">Widget-Links: Tap-Target auf 44 px bringen</div>
|
||||
<div class="fix__why">Der „Alle anzeigen →"-Link in jedem Dashboard-Widget hat keinen definierten <code>min-height</code>. Bei 12 px Text liegt der tatsächliche Klickbereich bei etwa 16–18 px — weit unter iOS-Minimum. Fix: padding + min-height in <code>dashboard.css</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-r"></span> Ist-Zustand</div>
|
||||
<div class="panel__body">
|
||||
<div class="widget-mock">
|
||||
<div class="wm-row">
|
||||
<div class="wm-title">Aufgaben</div>
|
||||
<a class="wm-link wm-link--bad">Alle →</a>
|
||||
</div>
|
||||
<div class="wm-body"></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#FCA5A5;margin-top:8px">Tap-Target ≈ 16 px Höhe</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Soll-Zustand</div>
|
||||
<div class="panel__body">
|
||||
<div class="widget-mock">
|
||||
<div class="wm-row">
|
||||
<div class="wm-title">Aufgaben</div>
|
||||
<a class="wm-link wm-link--good">Alle →</a>
|
||||
</div>
|
||||
<div class="wm-body"></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#6EE7B7;margin-top:8px">Tap-Target ≥ 32 px mit Padding</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-panel" style="margin-top:14px">
|
||||
<div class="code-panel__head"><span>dashboard.css · .widget__link</span></div>
|
||||
<div class="code-panel__body">
|
||||
<span class="add">+ .widget__link {</span>
|
||||
<span class="add">+ min-height: var(--target-base); <span class="cmt">/* 44px */</span></span>
|
||||
<span class="add">+ display: inline-flex;</span>
|
||||
<span class="add">+ align-items: center;</span>
|
||||
<span class="add">+ padding: 0 var(--space-2);</span>
|
||||
<span class="add">+ border-radius: var(--radius-sm);</span>
|
||||
<span class="add">+ }</span>
|
||||
<span class="add">+ .widget__link:hover {</span>
|
||||
<span class="add">+ background: color-mix(in srgb, var(--widget-accent) 10%, transparent);</span>
|
||||
<span class="add">+ }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 4: FAB -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-c">4</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCA5A5">Kritisch · 30 min</div>
|
||||
<div class="fix__title">FAB konsolidieren: .fab + .page-fab → eine Klasse</div>
|
||||
<div class="fix__why">Beide Klassen definieren fast denselben Button mit leicht abweichender <code>bottom</code>-Berechnung. Das führt zu inkonsistenter Positionierung. Die Lösung: eine Klasse, ein Token, alle Seiten konsistent.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-r"></span> Ist-Zustand: 2 Klassen</div>
|
||||
<div class="panel__body">
|
||||
<div class="fab-compare">
|
||||
<div class="fab-row">
|
||||
<div class="fab-circle fab-bad" style="background:rgba(252,165,165,.12)">+</div>
|
||||
<div class="fab-info">
|
||||
<div class="fab-name">.fab</div>
|
||||
<div class="fab-desc">bottom: nav-height + safe-area + space-4</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fab-row">
|
||||
<div class="fab-circle fab-bad" style="background:rgba(252,165,165,.12)">+</div>
|
||||
<div class="fab-info">
|
||||
<div class="fab-name">.page-fab</div>
|
||||
<div class="fab-desc">bottom: nav-bottom-height + 24px + safe-area</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#FCA5A5;margin-top:10px">Verschiedene bottom-Werte → visuell inkonsistent je nach Seite</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Soll-Zustand: 1 Klasse</div>
|
||||
<div class="panel__body">
|
||||
<div class="fab-compare">
|
||||
<div class="fab-row">
|
||||
<div class="fab-circle fab-good">+</div>
|
||||
<div class="fab-info">
|
||||
<div class="fab-name">.page-fab (Canonical)</div>
|
||||
<div class="fab-desc">Einheitliche bottom-Formel, alle Seiten</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#6EE7B7;margin-top:10px">Alle Seiten nutzen .page-fab, .fab wird entfernt oder als Alias gesetzt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<!-- ════════════════════════════════ HOCH ════════════════════════════════ -->
|
||||
|
||||
<!-- FIX 5: Widget Reorder -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-h">5</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCD34D">Hoch · 2–4 h</div>
|
||||
<div class="fix__title">Dashboard: Widget-Reihenfolge anpassbar machen</div>
|
||||
<div class="fix__why">Die Widget-Config speichert aktuell nur <code>{ id, visible }</code>. Ein <code>order</code>-Feld hinzufügen und im Customization-Modal drag-sortierbar machen (per <code>touch-action: none</code> + pointer events, kein externes Drag-Library nötig).</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-r"></span> Ist-Zustand: Feste Reihenfolge</div>
|
||||
<div class="panel__body">
|
||||
<div class="widget-order">
|
||||
<div class="wo-item" style="opacity:.5;cursor:default">
|
||||
<div class="wo-dot" style="background:#818CF8"></div>
|
||||
<div class="wo-label">Dashboard</div>
|
||||
<div class="wo-vis">⊙ sichtbar</div>
|
||||
</div>
|
||||
<div class="wo-item" style="opacity:.5;cursor:default">
|
||||
<div class="wo-dot" style="background:#15803D"></div>
|
||||
<div class="wo-label">Aufgaben</div>
|
||||
<div class="wo-vis">⊙ sichtbar</div>
|
||||
</div>
|
||||
<div class="wo-item" style="opacity:.5;cursor:default">
|
||||
<div class="wo-dot" style="background:#8250DF"></div>
|
||||
<div class="wo-label">Kalender</div>
|
||||
<div class="wo-vis">⊙ sichtbar</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#FCA5A5;margin-top:8px">Reihenfolge fest codiert — nicht änderbar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Soll-Zustand: Drag-to-reorder</div>
|
||||
<div class="panel__body">
|
||||
<div class="widget-order">
|
||||
<div class="wo-item">
|
||||
<div class="wo-handle">⠿</div>
|
||||
<div class="wo-dot" style="background:#818CF8"></div>
|
||||
<div class="wo-label">Dashboard</div>
|
||||
<div class="wo-vis">⊙</div>
|
||||
</div>
|
||||
<div class="wo-item" style="background:rgba(129,140,248,.08);border-color:rgba(129,140,248,.2)">
|
||||
<div class="wo-handle">⠿</div>
|
||||
<div class="wo-dot" style="background:#8250DF"></div>
|
||||
<div class="wo-label">Kalender</div>
|
||||
<div class="wo-vis">⊙</div>
|
||||
</div>
|
||||
<div class="wo-item">
|
||||
<div class="wo-handle">⠿</div>
|
||||
<div class="wo-dot" style="background:#15803D"></div>
|
||||
<div class="wo-label">Aufgaben</div>
|
||||
<div class="wo-vis">⊙</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#6EE7B7;margin-top:8px">⠿ Handle zum Sortieren — gespeichert in localStorage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-panel" style="margin-top:14px">
|
||||
<div class="code-panel__head"><span>dashboard.js — Config-Schema</span></div>
|
||||
<div class="code-panel__body">
|
||||
<span class="del">- const DEFAULT_WIDGET_CONFIG = WIDGET_IDS.map((id) => ({ id, visible: true }));</span>
|
||||
<span class="add">+ const DEFAULT_WIDGET_CONFIG = WIDGET_IDS.map((id, i) => ({ id, visible: true, order: i }));</span>
|
||||
<br>
|
||||
<span class="cmt">// Beim Laden: nach order sortieren</span>
|
||||
<span class="add">+ config.sort((a, b) => a.order - b.order);</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 6: Offline Indicator -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-h">6</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCD34D">Hoch · 1 h</div>
|
||||
<div class="fix__title">Offline-Banner in App-Shell</div>
|
||||
<div class="fix__why">Der Service Worker ist vorhanden, aber die App gibt kein visuelles Feedback zum Offline-Zustand. Ein kleines Banner direkt unter der Navigation (wenn <code>navigator.onLine</code> false ist) ist die robusteste Lösung — kein Flickering, immer sichtbar.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" style="margin-bottom:14px">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Soll-Zustand: Shell-Level Banner</div>
|
||||
<div class="panel__body">
|
||||
<div class="offline-banner">
|
||||
<span class="ob-icon">📡</span>
|
||||
<span class="ob-text">Offline — Änderungen werden gespeichert und beim nächsten Verbindungsaufbau synchronisiert.</span>
|
||||
<span class="ob-pill">Offline</span>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#8E8D89">Erscheint unter Nav-Bar, verschwindet automatisch wenn online</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-panel">
|
||||
<div class="code-panel__head"><span>router.js — App-Shell Setup</span></div>
|
||||
<div class="code-panel__body">
|
||||
<span class="add">+ function initOfflineBanner() {</span>
|
||||
<span class="add">+ const banner = document.getElementById('offline-banner');</span>
|
||||
<span class="add">+ const update = () => banner.hidden = navigator.onLine;</span>
|
||||
<span class="add">+ window.addEventListener('online', update);</span>
|
||||
<span class="add">+ window.addEventListener('offline', update);</span>
|
||||
<span class="add">+ update();</span>
|
||||
<span class="add">+ }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 7: Keyboard Shortcuts -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-h">7</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCD34D">Hoch · 2–3 h</div>
|
||||
<div class="fix__title">Globale Keyboard Shortcuts (Desktop)</div>
|
||||
<div class="fix__why">30 % der Nutzer verwenden Desktop. Ein zentrales Keyboard-Shortcut-System im Router beschleunigt häufige Aktionen erheblich. Alle Shortcuts per <code>?</code> einsehbar.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kbd-grid">
|
||||
<div class="kbd-row"><kbd class="kbd">/</kbd><span class="kbd-label">Suche öffnen</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">N</kbd><span class="kbd-label">Neu (kontextabhängig)</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">G D</kbd><span class="kbd-label">→ Dashboard</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">G T</kbd><span class="kbd-label">→ Tasks</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">G C</kbd><span class="kbd-label">→ Kalender</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">Esc</kbd><span class="kbd-label">Modal / Sheet schließen</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">?</kbd><span class="kbd-label">Shortcut-Übersicht</span></div>
|
||||
<div class="kbd-row"><kbd class="kbd">⌘K</kbd><span class="kbd-label">Command Palette</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 8: Undo System -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-h">8</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCD34D">Hoch · 3–4 h</div>
|
||||
<div class="fix__title">Zentrales Undo-System für destruktive Aktionen</div>
|
||||
<div class="fix__why">Aktuell haben manche Aktionen Undo-Toasts, andere nicht. Eine zentrale <code>undoStack</code>-Utility mit standardisiertem Toast-Muster schafft konsistente Sicherheit über alle Module hinweg.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head"><span class="dot dot-g"></span> Soll-Zustand: konsistente Undo-Abdeckung</div>
|
||||
<div class="panel__body">
|
||||
<div class="undo-arch">
|
||||
<div class="undo-row"><div class="undo-action">Aufgabe löschen</div><span class="undo-badge ub-yes">✓ Undo vorhanden</span></div>
|
||||
<div class="undo-row"><div class="undo-action">Eintrag Einkaufsliste löschen</div><span class="undo-badge ub-yes">✓ Undo vorhanden</span></div>
|
||||
<div class="undo-row"><div class="undo-action">Kontakt löschen</div><span class="undo-badge ub-no">✗ fehlt</span></div>
|
||||
<div class="undo-row"><div class="undo-action">Notiz löschen</div><span class="undo-badge ub-no">✗ fehlt</span></div>
|
||||
<div class="undo-row"><div class="undo-action">Geburtstag löschen</div><span class="undo-badge ub-no">✗ fehlt</span></div>
|
||||
<div class="undo-row"><div class="undo-action">Mahlzeit löschen</div><span class="undo-badge ub-no">✗ fehlt</span></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#8E8D89;margin-top:10px">→ Alle DELETE-Aktionen über eine zentrale <code>deleteWithUndo(url, label)</code> Funktion routen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- FIX 9: Onboarding -->
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-h">9</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#FCD34D">Hoch · 3 h</div>
|
||||
<div class="fix__title">Onboarding: Modul-spezifische Erst-Nutzung</div>
|
||||
<div class="fix__why">Statt 3 generischen Screens beim ersten Start: leere Zustände auf jeder Seite mit einem konkreten Tipp für genau dieses Modul. Zusätzlich: ein permanentes „?" / Hilfe-Icon in der Toolbar für den Onboarding-Replay.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ob-flow">
|
||||
<div class="ob-card ob-card--highlight">
|
||||
<div class="ob-ico">📋</div>
|
||||
<div class="ob-title">Aufgaben</div>
|
||||
<div class="ob-desc">Tippe + um deine erste Aufgabe zu erstellen. Wische links zum Löschen.</div>
|
||||
<div class="ob-dots"><div class="ob-dot ob-dot--on"></div><div class="ob-dot"></div><div class="ob-dot"></div></div>
|
||||
</div>
|
||||
<div class="ob-card">
|
||||
<div class="ob-ico">📅</div>
|
||||
<div class="ob-title">Kalender</div>
|
||||
<div class="ob-desc">Verbinde deinen Google Kalender unter Einstellungen → Kalender-Sync.</div>
|
||||
<div class="ob-dots"><div class="ob-dot"></div><div class="ob-dot ob-dot--on"></div><div class="ob-dot"></div></div>
|
||||
</div>
|
||||
<div class="ob-card">
|
||||
<div class="ob-ico">💰</div>
|
||||
<div class="ob-title">Budget</div>
|
||||
<div class="ob-desc">Lege Kategorien und ein Monatsbudget fest. Einnahmen und Ausgaben werden automatisch summiert.</div>
|
||||
<div class="ob-dots"><div class="ob-dot"></div><div class="ob-dot"></div><div class="ob-dot ob-dot--on"></div></div>
|
||||
</div>
|
||||
<div class="ob-card">
|
||||
<div class="ob-ico">🔔</div>
|
||||
<div class="ob-title">Erinnerungen</div>
|
||||
<div class="ob-desc">RRule ermöglicht Wiederholungen: täglich, wöchentlich, oder komplex wie „jeden 2. Montag".</div>
|
||||
</div>
|
||||
<div class="ob-card" style="opacity:.5">
|
||||
<div class="ob-ico">…</div>
|
||||
<div class="ob-title">+7 weitere</div>
|
||||
<div class="ob-desc">Jedes Modul erklärt sich beim ersten Besuch selbst.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ════════════════════════════════ MITTEL ════════════════════════════════ -->
|
||||
|
||||
<div class="fix">
|
||||
<div class="fix__header">
|
||||
<div class="fix__num num-m">10–13</div>
|
||||
<div class="fix__meta">
|
||||
<div class="fix__tag" style="color:#6EE7B7">Mittel · Backlog</div>
|
||||
<div class="fix__title">Weitere Optimierungen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel__head">🟢 Toast Swipe-to-Dismiss</div>
|
||||
<div class="panel__body">
|
||||
<p style="font-size:12px;color:#8E8D89;line-height:1.6">
|
||||
<code>pointerdown</code> + <code>pointermove</code> auf <code>.toast</code> → bei
|
||||
>40 px horizontaler Bewegung <code>toast--out</code> Klasse setzen und entfernen.
|
||||
Entspricht iOS/Android-Konvention. Ca. 30 Zeilen JS.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head">🟢 Swipe-Reveal auf alle Listen</div>
|
||||
<div class="panel__body">
|
||||
<p style="font-size:12px;color:#8E8D89;line-height:1.6">
|
||||
Das bestehende <code>.swipe-row</code> Pattern auf Kontakte, Notizen und Geburtstage
|
||||
ausweiten. Die Basis-CSS existiert bereits in <code>layout.css</code> —
|
||||
nur modul-spezifische Reveal-Farben und JS-Handler ergänzen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head">🟢 Dashboard: weniger Farbrauschen</div>
|
||||
<div class="panel__body">
|
||||
<p style="font-size:12px;color:#8E8D89;line-height:1.6">
|
||||
Widget-Icons auf neutrale <code>--color-text-secondary</code> setzen, Widget-Akzentfarbe
|
||||
nur für Badge und Link reservieren. Reduziert visuelle Überforderung wenn alle
|
||||
11 Widgets gleichzeitig sichtbar sind.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel__head">🟢 reminders.css lazy laden</div>
|
||||
<div class="panel__body">
|
||||
<p style="font-size:12px;color:#8E8D89;line-height:1.6">
|
||||
<code>reminders.css</code> dynamisch per <code><link rel="stylesheet"></code>
|
||||
nur in den Seiten laden, die Reminders anzeigen — analog zum bestehenden
|
||||
Lazy-Loading-Pattern für Page-Module.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user