Merge branch 'feature/birthday-reminders'
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
<!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 64px;
|
||||
}
|
||||
|
||||
.header {
|
||||
max-width: 900px;
|
||||
margin: 0 auto 40px;
|
||||
}
|
||||
|
||||
.header__eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: #818CF8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(28px, 5vw, 42px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h1 span { color: #818CF8; }
|
||||
|
||||
.header__sub {
|
||||
font-size: 15px;
|
||||
color: #8E8D89;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---- Section ---- */
|
||||
.section {
|
||||
max-width: 900px;
|
||||
margin: 0 auto 48px;
|
||||
}
|
||||
|
||||
.section__label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section__label::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.label--critical { color: #FCA5A5; }
|
||||
.label--high { color: #FCD34D; }
|
||||
.label--medium { color: #6EE7B7; }
|
||||
|
||||
/* ---- Issue Cards ---- */
|
||||
.issues {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.issue {
|
||||
background: #1a1a18;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.issue:hover {
|
||||
border-color: rgba(255,255,255,0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.issue__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.issue__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon--critical { background: rgba(252,165,165,0.15); }
|
||||
.icon--high { background: rgba(252,211,77,0.12); }
|
||||
.icon--medium { background: rgba(110,231,183,0.12); }
|
||||
|
||||
.issue__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0ede8;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.issue__body {
|
||||
font-size: 13px;
|
||||
color: #8E8D89;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.issue__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
align-self: flex-start;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.badge--critical { color: #FCA5A5; background: rgba(252,165,165,0.12); }
|
||||
.badge--high { color: #FCD34D; background: rgba(252,211,77,0.10); }
|
||||
.badge--medium { color: #6EE7B7; background: rgba(110,231,183,0.10); }
|
||||
|
||||
/* ---- Visual Demo: Navigation ---- */
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.demo-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.demo-box {
|
||||
background: #1a1a18;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-box__label {
|
||||
padding: 12px 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demo-box__label .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot--bad { background: #FCA5A5; }
|
||||
.dot--good { background: #6EE7B7; }
|
||||
|
||||
.demo-box__body { padding: 16px; }
|
||||
|
||||
/* Sidebar simulation */
|
||||
.sim-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 64px;
|
||||
background: #222220;
|
||||
border-radius: 12px;
|
||||
padding: 12px 8px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.sim-sidebar-wide {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.sim-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #8E8D89;
|
||||
}
|
||||
|
||||
.sim-nav-item--active {
|
||||
background: rgba(129,140,248,0.15);
|
||||
color: #818CF8;
|
||||
}
|
||||
|
||||
.sim-nav-icon {
|
||||
width: 20px; height: 20px;
|
||||
background: currentColor;
|
||||
border-radius: 4px;
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sim-nav-item--active .sim-nav-icon { opacity: 1; }
|
||||
|
||||
/* Touch target demo */
|
||||
.touch-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.touch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.touch-btn {
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #f0ede8;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.touch-btn--bad {
|
||||
width: 40px; height: 40px;
|
||||
border-color: rgba(252,165,165,0.4);
|
||||
background: rgba(252,165,165,0.08);
|
||||
color: #FCA5A5;
|
||||
}
|
||||
|
||||
.touch-btn--good {
|
||||
width: 44px; height: 44px;
|
||||
border-color: rgba(110,231,183,0.4);
|
||||
background: rgba(110,231,183,0.08);
|
||||
color: #6EE7B7;
|
||||
}
|
||||
|
||||
.touch-label {
|
||||
font-size: 12px;
|
||||
color: #8E8D89;
|
||||
}
|
||||
|
||||
.touch-label strong { color: #f0ede8; font-weight: 600; }
|
||||
|
||||
/* Widget touch demo */
|
||||
.widget-link-demo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: #222220;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wl-title { font-size: 13px; font-weight: 600; }
|
||||
|
||||
.wl-link--bad {
|
||||
font-size: 11px;
|
||||
color: #818CF8;
|
||||
padding: 2px 0;
|
||||
/* No min-height — very small tap target */
|
||||
}
|
||||
|
||||
.wl-link--good {
|
||||
font-size: 11px;
|
||||
color: #6EE7B7;
|
||||
padding: 8px 10px;
|
||||
background: rgba(110,231,183,0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Onboarding demo */
|
||||
.onboarding-demo {
|
||||
background: #222220;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onboarding-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.o-step {
|
||||
width: 8px; height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.o-step--active { background: #818CF8; width: 20px; border-radius: 4px; }
|
||||
|
||||
.onboarding-icon {
|
||||
width: 48px; height: 48px;
|
||||
border-radius: 14px;
|
||||
background: rgba(129,140,248,0.15);
|
||||
margin: 0 auto 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.o-count {
|
||||
font-size: 11px;
|
||||
color: #8E8D89;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Module grid demo (more-sheet) */
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #222220;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.module-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 6px;
|
||||
border-radius: 10px;
|
||||
background: #2A2A28;
|
||||
font-size: 10px;
|
||||
color: #8E8D89;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
max-width: 900px;
|
||||
margin: 0 auto 40px;
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
/* Summary bar */
|
||||
.summary {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(135deg, rgba(129,140,248,0.15), rgba(167,139,250,0.08));
|
||||
border: 1px solid rgba(129,140,248,0.2);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.summary { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.summary__num {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary__label {
|
||||
font-size: 12px;
|
||||
color: #8E8D89;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.num--critical { color: #FCA5A5; }
|
||||
.num--high { color: #FCD34D; }
|
||||
.num--medium { color: #6EE7B7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div class="header__eyebrow">UX/UI Analyse · Oikos · April 2026</div>
|
||||
<h1>Was funktioniert gut —<br>und was bremst <span>Oikos</span> aus</h1>
|
||||
<p class="header__sub">70 % Mobile-PWA · 30 % Desktop · 11 Module · Vanilla JS · Kein Build-Step</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="section">
|
||||
<div class="summary">
|
||||
<div>
|
||||
<div class="summary__num num--critical">4</div>
|
||||
<div class="summary__label">Kritische Probleme<br>(sofort beheben)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="summary__num num--high">5</div>
|
||||
<div class="summary__label">Hohe Priorität<br>(nächster Sprint)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="summary__num num--medium">4</div>
|
||||
<div class="summary__label">Mittlere Priorität<br>(Backlog)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- KRITISCH -->
|
||||
<div class="section">
|
||||
<div class="section__label label--critical">🔴 Kritisch — direkt sichtbare UX-Brüche</div>
|
||||
<div class="issues">
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--critical">🧭</div>
|
||||
<div class="issue__title">Sidebar 1024–1279 px: Icons ohne Labels oder Tooltips</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Bei der häufigsten Desktop-Auflösung zeigt die Sidebar nur Icons — kein Tooltip, kein Label.
|
||||
Neue Nutzer können die 11 Module nicht erkennen. Das verletzt das Grundprinzip
|
||||
«nav-label-icon» (HIG/Material).
|
||||
</div>
|
||||
<div class="issue__badge badge--critical">Keine Discoverability</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--critical">👆</div>
|
||||
<div class="issue__title">Modal-Close: 40 px statt 44 px Minimum</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
<code>.modal-panel__close</code> nutzt <code>--target-md</code> (40 px).
|
||||
Das iOS-Minimum liegt bei 44 pt. Auf kleinen Displays — gerade beim
|
||||
Schließen tippend — ist das ein spürbares Frustrationspotenzial.
|
||||
</div>
|
||||
<div class="issue__badge badge--critical">Apple HIG Violation</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--critical">🔗</div>
|
||||
<div class="issue__title">Widget-Links: kein Min-Height-Tap-Target</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
<code>.widget__link</code> hat 12 px Text, kein explizites <code>min-height</code>.
|
||||
Der effektive Tippbereich ist ~16–18 px — weit unter 44 px.
|
||||
Auf dem Dashboard ist dieser Link auf jedem Widget sichtbar.
|
||||
</div>
|
||||
<div class="issue__badge badge--critical">Tap-Target < 44 px</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--critical">🔁</div>
|
||||
<div class="issue__title">Doppelter FAB: .fab vs .page-fab</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
In <code>layout.css</code> existieren zwei nahezu identische FAB-Klassen (<code>.fab</code>
|
||||
und <code>.page-fab</code>) mit unterschiedlicher <code>bottom</code>-Berechnung.
|
||||
Das erzeugt inkonsistente Positionierung auf verschiedenen Seiten.
|
||||
</div>
|
||||
<div class="issue__badge badge--critical">Inkonsistente UI</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visuelle Demo: Navigation -->
|
||||
<div class="section">
|
||||
<div class="section__label label--critical" style="color:#8E8D89">↳ Visuell: Navigation-Problem & Lösung</div>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-box">
|
||||
<div class="demo-box__label">
|
||||
<span class="dot dot--bad"></span>
|
||||
Ist-Zustand: 1024–1279 px (nur Icons)
|
||||
</div>
|
||||
<div class="demo-box__body">
|
||||
<div class="sim-sidebar">
|
||||
<div class="sim-nav-item sim-nav-item--active">
|
||||
<div class="sim-nav-icon"></div>
|
||||
</div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#8E8D89;text-align:center;margin-top:10px">Welcher Icon ist "Geburtstage"?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-box">
|
||||
<div class="demo-box__label">
|
||||
<span class="dot dot--good"></span>
|
||||
Soll-Zustand: Tooltip bei Hover (min)
|
||||
</div>
|
||||
<div class="demo-box__body">
|
||||
<div class="sim-sidebar" style="position:relative">
|
||||
<div class="sim-nav-item sim-nav-item--active" style="position:relative">
|
||||
<div class="sim-nav-icon"></div>
|
||||
<div style="position:absolute;left:52px;top:50%;transform:translateY(-50%);background:#333;padding:4px 10px;border-radius:6px;font-size:11px;color:#f0ede8;white-space:nowrap;z-index:10;border:1px solid rgba(255,255,255,0.1)">Dashboard</div>
|
||||
</div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#6EE7B7;text-align:center;margin-top:10px">Sofort klar durch title-Tooltip</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Touch Demo -->
|
||||
<div class="section" style="margin-top:0">
|
||||
<div class="section__label label--critical" style="color:#8E8D89">↳ Visuell: Touch-Target Größen</div>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-box">
|
||||
<div class="demo-box__label">
|
||||
<span class="dot dot--bad"></span>
|
||||
Ist-Zustand: Modal-Close 40 × 40 px
|
||||
</div>
|
||||
<div class="demo-box__body">
|
||||
<div class="touch-demo">
|
||||
<div class="touch-row">
|
||||
<div class="touch-btn touch-btn--bad">✕</div>
|
||||
<div class="touch-label"><strong>40 × 40 px</strong> — 4 px unter iOS-Minimum</div>
|
||||
</div>
|
||||
<div class="touch-row" style="margin-top:4px">
|
||||
<div style="font-size:11px;color:#FCA5A5;padding:4px 10px;background:rgba(252,165,165,0.08);border-radius:6px">
|
||||
Fingertipp ~44–50 px → Fehlklick-Rate steigt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-box">
|
||||
<div class="demo-box__label">
|
||||
<span class="dot dot--good"></span>
|
||||
Fix: --target-base (44 px)
|
||||
</div>
|
||||
<div class="demo-box__body">
|
||||
<div class="touch-demo">
|
||||
<div class="touch-row">
|
||||
<div class="touch-btn touch-btn--good">✕</div>
|
||||
<div class="touch-label"><strong>44 × 44 px</strong> — Apple HIG Minimum</div>
|
||||
</div>
|
||||
<div class="touch-row" style="margin-top:4px">
|
||||
<div style="font-size:11px;color:#6EE7B7;padding:4px 10px;background:rgba(110,231,183,0.08);border-radius:6px">
|
||||
Einzeiler-Fix: <code>--target-base</code> statt <code>--target-md</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- HOCH -->
|
||||
<div class="section">
|
||||
<div class="section__label label--high">🟡 Hohe Priorität — spürbare UX-Einschränkungen</div>
|
||||
<div class="issues">
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--high">🗂️</div>
|
||||
<div class="issue__title">Onboarding: 3 Schritte für 11 Module</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Neue Nutzer sehen einmalig 3 generische Onboarding-Screens.
|
||||
Module wie Budget, RRule-Wiederholungen oder Google Calendar-Sync
|
||||
werden nie erklärt. Kein Feature-Discovery-Mechanismus danach.
|
||||
</div>
|
||||
<div class="issue__badge badge--high">Discoverability</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--high">📊</div>
|
||||
<div class="issue__title">Dashboard-Widgets: keine Reihenfolge-Anpassung</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Widgets können ein-/ausgeblendet werden, aber nicht umsortiert.
|
||||
Die Widget-Config speichert nur <code>{ id, visible }</code> — kein
|
||||
Reihenfolge-Feld. Für Familien mit verschiedenen Prioritäten ist das stark limitierend.
|
||||
</div>
|
||||
<div class="issue__badge badge--high">Personalisierung</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--high">↩️</div>
|
||||
<div class="issue__title">Inkonsistentes Undo-Verhalten</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Einige Aktionen zeigen einen Undo-Toast, andere nicht.
|
||||
Es gibt kein zentrales Undo-System. Bei destruktiven Aktionen
|
||||
(z. B. Kontakt löschen) fehlt der Undo-Pfad komplett.
|
||||
</div>
|
||||
<div class="issue__badge badge--high">Fehlererholung</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--high">📡</div>
|
||||
<div class="issue__title">Kein sichtbarer Offline-Indikator</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Der Service Worker existiert, aber der App-Shell fehlt ein
|
||||
Offline-Banner. Nutzer bemerken den Offline-Zustand erst,
|
||||
wenn eine API-Anfrage fehlschlägt — zu spät.
|
||||
</div>
|
||||
<div class="issue__badge badge--high">PWA-Erfahrung</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--high">⌨️</div>
|
||||
<div class="issue__title">Desktop: keine Keyboard Shortcuts</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Bei 30 % Desktop-Nutzung fehlen globale Shortcuts.
|
||||
Kein „N" für neue Aufgabe, kein „/" für Suche, kein
|
||||
Escape-Verhalten im globalem Kontext. Power-User müssen
|
||||
alles mit der Maus bedienen.
|
||||
</div>
|
||||
<div class="issue__badge badge--high">Desktop-Ergonomie</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- MITTEL -->
|
||||
<div class="section">
|
||||
<div class="section__label label--medium">🟢 Mittlere Priorität — qualitative Verbesserungen</div>
|
||||
<div class="issues">
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--medium">💬</div>
|
||||
<div class="issue__title">reminders.css global geladen</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
<code>reminders.css</code> wird laut Observations global geladen,
|
||||
nicht lazy. Auf Seiten ohne Reminder-UI werden unnötige Styles
|
||||
geparst. Kein Blocking-Problem, aber vermeidbare CSS-Last.
|
||||
</div>
|
||||
<div class="issue__badge badge--medium">Performance</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--medium">🃏</div>
|
||||
<div class="issue__title">Swipe-Geste: nur Tasks & Shopping</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Swipe-Reveal ist für Tasks und Shopping implementiert,
|
||||
fehlt aber bei Kontakten, Notizen und Geburtstagen — obwohl
|
||||
die Interaktion dort genauso wertvoll wäre.
|
||||
Inkonsistente Erwartungshaltung.
|
||||
</div>
|
||||
<div class="issue__badge badge--medium">Interaktions-Konsistenz</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--medium">🎨</div>
|
||||
<div class="issue__title">11 Modulfarben gleichzeitig im Dashboard</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Wenn alle Widgets sichtbar sind, treffen 11 verschiedene
|
||||
Akzentfarben aufeinander. Das Dashboard wirkt farblich
|
||||
überladen. Weniger Kontrast zwischen den Modulen würde
|
||||
die Ruhewirkung verbessern.
|
||||
</div>
|
||||
<div class="issue__badge badge--medium">Visuelle Ruhe</div>
|
||||
</div>
|
||||
|
||||
<div class="issue">
|
||||
<div class="issue__header">
|
||||
<div class="issue__icon icon--medium">📱</div>
|
||||
<div class="issue__title">Toast: kein Swipe-to-Dismiss auf Mobile</div>
|
||||
</div>
|
||||
<div class="issue__body">
|
||||
Toasts können nicht weggewischt werden — nur auto-dismiss
|
||||
oder Undo-Button. Auf Mobile ist Swipe-to-Dismiss eine
|
||||
etablierte Konvention (iOS, Android), deren Fehlen auffällt.
|
||||
</div>
|
||||
<div class="issue__badge badge--medium">Mobile-Konvention</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Stärken -->
|
||||
<div class="section">
|
||||
<div class="section__label" style="color:#818CF8">✨ Was bereits ausgezeichnet ist</div>
|
||||
<div class="issues">
|
||||
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
|
||||
<div class="issue__title" style="margin-bottom:6px">🌙 Dark Mode</div>
|
||||
<div class="issue__body">Private-/Public-Token-Architektur ist mustergültig. Alle Kontrastverhältnisse WCAG-geprüft, Toast-Texte passen sich an.</div>
|
||||
</div>
|
||||
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
|
||||
<div class="issue__title" style="margin-bottom:6px">♿ Accessibility-Schichten</div>
|
||||
<div class="issue__body">prefers-reduced-motion, prefers-reduced-transparency, prefers-contrast, forced-colors — alle implementiert. Selten in selbstgehosteten Apps.</div>
|
||||
</div>
|
||||
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
|
||||
<div class="issue__title" style="margin-bottom:6px">🍎 iOS-PWA-Bewusstsein</div>
|
||||
<div class="issue__body">100dvh + -webkit-fill-available Fallback, safe-area-inset-*, Flex-Kind statt fixed für Bottom-Nav — solide PWA-Grundlage.</div>
|
||||
</div>
|
||||
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
|
||||
<div class="issue__title" style="margin-bottom:6px">💎 Glass Design System</div>
|
||||
<div class="issue__body">@supports-basiert mit korrekten webkit-Fallbacks, opake Fallbacks für reduced-transparency. Konsistente Token-Hierarchie.</div>
|
||||
</div>
|
||||
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
|
||||
<div class="issue__title" style="margin-bottom:6px">🎭 Modul-Theming</div>
|
||||
<div class="issue__body">--active-module-accent + --module-accent System ist elegant. FAB, Nav-Item und Toggles reflektieren automatisch die aktive Seite.</div>
|
||||
</div>
|
||||
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
|
||||
<div class="issue__title" style="margin-bottom:6px">📐 Design Tokens</div>
|
||||
<div class="issue__body">Vollständige Skala für Farben, Radien, Schatten, Spacing, Typografie. Konsistente Anwendung — kaum Hardcoding gefunden.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Inject into brainstorm frame if available
|
||||
if (window.parent !== window) {
|
||||
// signal ready
|
||||
window.parent.postMessage({ type: 'screen-ready' }, '*');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1777305983821}
|
||||
@@ -0,0 +1 @@
|
||||
947907
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1777311074842}
|
||||
@@ -0,0 +1 @@
|
||||
958547
|
||||
@@ -20,6 +20,50 @@ function initials(name) {
|
||||
.join('') || '?';
|
||||
}
|
||||
|
||||
const REMINDER_OFFSETS = () => [
|
||||
{ value: '', label: t('reminders.offsetNone') },
|
||||
{ value: '0', label: t('reminders.offsetAtTime') },
|
||||
{ value: '15', label: t('reminders.offset15min') },
|
||||
{ value: '60', label: t('reminders.offset1hour') },
|
||||
{ value: '1440', label: t('reminders.offset1day') },
|
||||
{ value: '2880', label: t('reminders.offset2days') },
|
||||
{ value: '10080', label: t('reminders.offset1week') },
|
||||
{ value: '20160', label: t('reminders.offset2weeks') },
|
||||
{ value: 'custom', label: t('reminders.offsetCustom') },
|
||||
];
|
||||
|
||||
function renderBirthdayReminderSection(birthday = null) {
|
||||
const currentOffset = birthday?.reminder_offset ?? '0';
|
||||
const customAmount = birthday?.reminder_custom_amount || 1;
|
||||
const customUnit = birthday?.reminder_custom_unit || 'days';
|
||||
return `
|
||||
<div class="reminder-section">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label class="form-label" for="bd-reminder-offset">${t('reminders.offsetLabel')}</label>
|
||||
<select class="form-input" id="bd-reminder-offset" style="min-height:44px">
|
||||
${REMINDER_OFFSETS().map((o) =>
|
||||
`<option value="${o.value}" ${currentOffset === o.value ? 'selected' : ''}>${esc(o.label)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-grid modal-grid--2 reminder-custom" id="bd-reminder-custom" ${currentOffset === 'custom' ? '' : 'hidden'}>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label class="form-label" for="bd-reminder-custom-amount">${t('reminders.customAmountLabel')}</label>
|
||||
<input class="form-input" type="number" id="bd-reminder-custom-amount" min="1" max="999" value="${customAmount}">
|
||||
</div>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label class="form-label" for="bd-reminder-custom-unit">${t('reminders.customUnitLabel')}</label>
|
||||
<select class="form-input" id="bd-reminder-custom-unit">
|
||||
<option value="minutes" ${customUnit === 'minutes' ? 'selected' : ''}>${t('reminders.customMinutes')}</option>
|
||||
<option value="hours" ${customUnit === 'hours' ? 'selected' : ''}>${t('reminders.customHours')}</option>
|
||||
<option value="days" ${customUnit === 'days' ? 'selected' : ''}>${t('reminders.customDays')}</option>
|
||||
<option value="weeks" ${customUnit === 'weeks' ? 'selected' : ''}>${t('reminders.customWeeks')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function ageNote(birthday) {
|
||||
if (birthday.days_until === 0) return t('birthdays.ageNoteToday', { age: birthday.next_age });
|
||||
if (birthday.days_until === 1) return t('birthdays.ageNoteTomorrow', { age: birthday.next_age });
|
||||
@@ -327,6 +371,7 @@ function openBirthdayModal({ mode, birthday = null }) {
|
||||
<label class="form-label" for="bd-notes">${t('birthdays.notesLabel')}</label>
|
||||
<textarea class="form-input" id="bd-notes" rows="3" placeholder="${t('birthdays.notesPlaceholder')}">${esc(birthday?.notes || '')}</textarea>
|
||||
</div>
|
||||
${renderBirthdayReminderSection(birthday)}
|
||||
<div class="birthday-modal__hint">${t('birthdays.calendarHint')}</div>
|
||||
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
||||
${isEdit ? `<button class="btn btn--danger" id="bd-delete">${t('common.delete')}</button>` : '<div></div>'}
|
||||
@@ -365,6 +410,13 @@ function openBirthdayModal({ mode, birthday = null }) {
|
||||
if (fileInput) fileInput.value = '';
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
const reminderOffset = panel.querySelector('#bd-reminder-offset');
|
||||
const reminderCustom = panel.querySelector('#bd-reminder-custom');
|
||||
reminderOffset?.addEventListener('change', () => {
|
||||
if (reminderCustom) reminderCustom.hidden = reminderOffset.value !== 'custom';
|
||||
});
|
||||
|
||||
panel.querySelector('#bd-cancel').addEventListener('click', closeModal);
|
||||
panel.querySelector('#bd-delete')?.addEventListener('click', async () => {
|
||||
closeModal();
|
||||
@@ -379,6 +431,9 @@ function openBirthdayModal({ mode, birthday = null }) {
|
||||
birth_date: birthDate,
|
||||
notes: panel.querySelector('#bd-notes').value.trim(),
|
||||
photo_data: photoData,
|
||||
reminder_offset: panel.querySelector('#bd-reminder-offset').value,
|
||||
reminder_custom_amount: panel.querySelector('#bd-reminder-custom-amount').value,
|
||||
reminder_custom_unit: panel.querySelector('#bd-reminder-custom-unit').value,
|
||||
};
|
||||
|
||||
if (!body.name || !body.birth_date || !isDateInputValid(birthDateRaw)) {
|
||||
|
||||
@@ -754,14 +754,6 @@
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.reminder-custom {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.reminder-custom[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.agenda-event__meta {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@@ -92,3 +92,11 @@
|
||||
.reminder-fields .modal-grid {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.reminder-custom {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.reminder-custom[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
if (url.pathname.startsWith('/api/')) return;
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
|
||||
@@ -1180,6 +1180,15 @@ const MIGRATIONS = [
|
||||
CREATE INDEX idx_contact_addresses_contact ON contact_addresses(contact_id);
|
||||
`,
|
||||
},
|
||||
{
|
||||
version: 31,
|
||||
description: 'Advanced reminder options for birthdays',
|
||||
up: `
|
||||
ALTER TABLE birthdays ADD COLUMN reminder_offset TEXT;
|
||||
ALTER TABLE birthdays ADD COLUMN reminder_custom_amount INTEGER;
|
||||
ALTER TABLE birthdays ADD COLUMN reminder_custom_unit TEXT;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,9 +75,18 @@ router.post('/', (req, res) => {
|
||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||
|
||||
const result = db.get().prepare(`
|
||||
INSERT INTO birthdays (name, birth_date, notes, photo_data, created_by)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(vName.value, vBirthDate.value, vNotes.value, vPhoto.value ?? null, req.authUserId || req.session.userId);
|
||||
INSERT INTO birthdays (name, birth_date, notes, photo_data, created_by, reminder_offset, reminder_custom_amount, reminder_custom_unit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
vName.value,
|
||||
vBirthDate.value,
|
||||
vNotes.value,
|
||||
vPhoto.value ?? null,
|
||||
req.authUserId || req.session.userId,
|
||||
req.body.reminder_offset ?? null,
|
||||
req.body.reminder_custom_amount ?? null,
|
||||
req.body.reminder_custom_unit ?? null
|
||||
);
|
||||
|
||||
const birthday = loadBirthday(result.lastInsertRowid);
|
||||
const synced = db.transaction(() => syncBirthdayArtifacts(db.get(), birthday));
|
||||
@@ -111,6 +120,9 @@ router.put('/:id', (req, res) => {
|
||||
birth_date = COALESCE(?, birth_date),
|
||||
notes = ?,
|
||||
photo_data = ?,
|
||||
reminder_offset = ?,
|
||||
reminder_custom_amount = ?,
|
||||
reminder_custom_unit = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
@@ -118,6 +130,9 @@ router.put('/:id', (req, res) => {
|
||||
req.body.birth_date ?? null,
|
||||
req.body.notes !== undefined ? (req.body.notes?.trim() || null) : existing.notes,
|
||||
req.body.photo_data !== undefined ? (vPhoto.value ?? null) : existing.photo_data,
|
||||
req.body.reminder_offset !== undefined ? req.body.reminder_offset : existing.reminder_offset,
|
||||
req.body.reminder_custom_amount !== undefined ? req.body.reminder_custom_amount : existing.reminder_custom_amount,
|
||||
req.body.reminder_custom_unit !== undefined ? req.body.reminder_custom_unit : existing.reminder_custom_unit,
|
||||
id,
|
||||
);
|
||||
|
||||
|
||||
@@ -43,9 +43,22 @@ function daysUntilBirthday(birthDate, from = new Date()) {
|
||||
return Math.round((nextUtc - todayUtc) / 86400000);
|
||||
}
|
||||
|
||||
function birthdayReminderAt(birthDate, from = new Date()) {
|
||||
function getOffsetMinutes(birthday) {
|
||||
if (birthday.reminder_offset === 'custom') {
|
||||
const amount = parseInt(birthday.reminder_custom_amount, 10) || 1;
|
||||
const unit = birthday.reminder_custom_unit || 'days';
|
||||
if (unit === 'weeks') return amount * 10080;
|
||||
if (unit === 'days') return amount * 1440;
|
||||
if (unit === 'hours') return amount * 60;
|
||||
return amount;
|
||||
}
|
||||
return parseInt(birthday.reminder_offset, 10) || 0;
|
||||
}
|
||||
|
||||
function birthdayReminderAt(birthDate, offsetMin = 0, from = new Date()) {
|
||||
const next = nextBirthdayDate(birthDate, from);
|
||||
return `${next}T12:00:00Z`;
|
||||
const baseTime = new Date(`${next}T12:00:00Z`).getTime();
|
||||
return new Date(baseTime - (offsetMin || 0) * 60000).toISOString();
|
||||
}
|
||||
|
||||
function eventTitle(name) {
|
||||
@@ -125,7 +138,16 @@ function syncBirthdayCalendarEvent(database, birthday) {
|
||||
function syncBirthdayReminder(database, birthday, from = new Date()) {
|
||||
if (!birthday.calendar_event_id) return null;
|
||||
|
||||
const desired = birthdayReminderAt(birthday.birth_date, from);
|
||||
if (birthday.reminder_offset === '') {
|
||||
database.prepare(`
|
||||
DELETE FROM reminders
|
||||
WHERE entity_type = 'event' AND entity_id = ? AND created_by = ?
|
||||
`).run(birthday.calendar_event_id, birthday.created_by);
|
||||
return null;
|
||||
}
|
||||
|
||||
const offsetMin = getOffsetMinutes(birthday);
|
||||
const desired = birthdayReminderAt(birthday.birth_date, offsetMin, from);
|
||||
const existing = database.prepare(`
|
||||
SELECT * FROM reminders
|
||||
WHERE entity_type = 'event' AND entity_id = ? AND created_by = ?
|
||||
|
||||
Reference in New Issue
Block a user