82a1f2c239
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>
630 lines
32 KiB
HTML
630 lines
32 KiB
HTML
<!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>
|