feat(ux): first-time onboarding overlay for new users

Shows a 3-screen modal overlay on first dashboard visit explaining key
features; dismissed state is persisted to localStorage so it never
reappears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-04-26 23:29:57 +02:00
parent 93ac635835
commit 6cf6b9bec0
17 changed files with 358 additions and 0 deletions
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "الإشعارات نشطة",
"notificationDenied": "الإشعارات محظورة",
"notificationHint": "احصل على إشعارات حتى عندما يكون التطبيق مفتوحًا."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"duplicate": "Duplizieren",
"duplicated": "Rezept dupliziert.",
"copySuffix": "Kopie"
},
"onboarding": {
"step1Title": "Willkommen bei Oikos",
"step1Body": "Dein persönlicher Familienplaner. Aufgaben, Kalender, Einkauf und mehr alles an einem Ort.",
"step2Title": "Alles im Blick",
"step2Body": "Über die Navigation unten erreichst du alle Module. Mit dem +-Button erstellst du schnell neue Einträge.",
"step3Title": "Bereit loszulegen",
"step3Body": "Das Dashboard zeigt dir die wichtigsten Infos auf einen Blick. Du kannst es unter \"Anpassen\" nach deinen Wünschen einrichten.",
"next": "Weiter",
"done": "Loslegen",
"skip": "Überspringen"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Ειδοποιήσεις ενεργές",
"notificationDenied": "Ειδοποιήσεις αποκλεισμένες",
"notificationHint": "Λάβετε ειδοποιήσεις ακόμα και όταν η εφαρμογή είναι ανοιχτή."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"open": "Open search",
"placeholder": "Search…",
"noResults": "No results found."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Notificaciones activas",
"notificationDenied": "Notificaciones bloqueadas",
"notificationHint": "Recibe notificaciones incluso cuando la aplicación está abierta."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Notifications actives",
"notificationDenied": "Notifications bloquées",
"notificationHint": "Recevez des notifications même lorsque l'application est ouverte."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "सूचनाएं सक्रिय",
"notificationDenied": "सूचनाएं अवरुद्ध",
"notificationHint": "ऐप खुली होने पर भी सूचनाएं प्राप्त करें।"
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Notifiche attive",
"notificationDenied": "Notifiche bloccate",
"notificationHint": "Ricevi notifiche anche quando l'app è aperta."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "通知が有効",
"notificationDenied": "通知がブロックされています",
"notificationHint": "アプリが開いているときでも通知を受け取ります。"
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Notificações ativas",
"notificationDenied": "Notificações bloqueadas",
"notificationHint": "Receba notificações mesmo quando a aplicação está aberta."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Уведомления активны",
"notificationDenied": "Уведомления заблокированы",
"notificationHint": "Получайте уведомления, даже когда приложение открыто."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Notiser aktiva",
"notificationDenied": "Notiser blockerade",
"notificationHint": "Få notiser även när appen är öppen."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "Bildirimler etkin",
"notificationDenied": "Bildirimler engellendi",
"notificationHint": "Uygulama açıkken bile bildirim alın."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"open": "Відкрити пошук",
"placeholder": "Пошук…",
"noResults": "Результатів не знайдено."
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+11
View File
@@ -780,5 +780,16 @@
"notificationEnabled": "通知已启用",
"notificationDenied": "通知已被阻止",
"notificationHint": "即使应用程序打开时也能收到通知。"
},
"onboarding": {
"step1Title": "Welcome to Oikos",
"step1Body": "Your personal family planner. Tasks, calendar, shopping and more all in one place.",
"step2Title": "Everything at a glance",
"step2Body": "Use the navigation below to reach all modules. The + button creates new entries quickly.",
"step3Title": "Ready to go",
"step3Body": "The dashboard shows you the most important information at a glance. Customize it under \"Customize\".",
"next": "Next",
"done": "Get started",
"skip": "Skip"
}
}
+98
View File
@@ -12,6 +12,100 @@ import { openModal, closeModal } from '/components/modal.js';
// Hält den AbortController des aktuellen FAB-Listeners - wird bei jedem render() erneuert.
let _fabController = null;
// ── Onboarding ──────────────────────────────────────────────────────────────
const ONBOARDING_KEY = 'oikos-onboarded';
function getOnboardingSteps() {
return [
{ icon: 'home', title: t('onboarding.step1Title'), body: t('onboarding.step1Body') },
{ icon: 'grid-2x2', title: t('onboarding.step2Title'), body: t('onboarding.step2Body') },
{ icon: 'circle-check', title: t('onboarding.step3Title'), body: t('onboarding.step3Body') },
];
}
function showOnboarding(appContainer) {
const steps = getOnboardingSteps();
let current = 0;
const overlay = document.createElement('div');
overlay.className = 'onboarding-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
function renderStep() {
const step = steps[current];
const isLast = current === steps.length - 1;
overlay.replaceChildren();
const card = document.createElement('div');
card.className = 'onboarding-card';
const icon = document.createElement('i');
icon.dataset.lucide = step.icon;
icon.className = 'onboarding-icon';
icon.setAttribute('aria-hidden', 'true');
const title = document.createElement('h2');
title.className = 'onboarding-title';
title.textContent = step.title;
const body = document.createElement('p');
body.className = 'onboarding-body';
body.textContent = step.body;
const dots = document.createElement('div');
dots.className = 'onboarding-dots';
steps.forEach((_, i) => {
const dot = document.createElement('span');
dot.className = `onboarding-dot${i === current ? ' onboarding-dot--active' : ''}`;
dots.appendChild(dot);
});
const actions = document.createElement('div');
actions.className = 'onboarding-actions';
const skipBtn = document.createElement('button');
skipBtn.className = 'btn btn--ghost';
skipBtn.textContent = t('onboarding.skip');
skipBtn.addEventListener('click', finish);
const nextBtn = document.createElement('button');
nextBtn.className = 'btn btn--primary';
nextBtn.textContent = isLast ? t('onboarding.done') : t('onboarding.next');
nextBtn.addEventListener('click', () => {
if (isLast) { finish(); return; }
current++;
renderStep();
if (window.lucide) window.lucide.createIcons({ el: overlay });
nextBtn.focus();
});
actions.appendChild(skipBtn);
actions.appendChild(nextBtn);
card.appendChild(icon);
card.appendChild(title);
card.appendChild(body);
card.appendChild(dots);
card.appendChild(actions);
overlay.appendChild(card);
if (window.lucide) window.lucide.createIcons({ el: overlay });
setTimeout(() => nextBtn.focus(), 50);
}
function finish() {
localStorage.setItem(ONBOARDING_KEY, '1');
overlay.classList.add('onboarding-overlay--out');
overlay.addEventListener('animationend', () => overlay.remove(), { once: true });
// Fallback falls animationend nicht feuert (prefers-reduced-motion):
setTimeout(() => overlay.remove(), 300);
}
renderStep();
appContainer.appendChild(overlay);
}
// --------------------------------------------------------
// Widget-Definitionen (Reihenfolge = Standard-Layout)
// --------------------------------------------------------
@@ -823,6 +917,10 @@ export async function render(container, { user }) {
const timerId = setInterval(doAutoRefresh, 30 * 60 * 1000);
_fabController.signal.addEventListener('abort', () => clearInterval(timerId));
}
if (!localStorage.getItem(ONBOARDING_KEY)) {
setTimeout(() => showOnboarding(container), 400);
}
}
function wireWeatherRefresh(container) {
+95
View File
@@ -1220,3 +1220,98 @@
opacity: 0.3;
cursor: not-allowed;
}
/* ── Onboarding Overlay ── */
.onboarding-overlay {
position: fixed;
inset: 0;
z-index: var(--z-modal);
background: color-mix(in srgb, var(--color-bg) 70%, transparent);
backdrop-filter: var(--blur-lg);
-webkit-backdrop-filter: var(--blur-lg);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-6);
animation: onboarding-in 0.3s var(--ease-out);
}
.onboarding-overlay--out {
animation: onboarding-out 0.25s ease forwards;
}
@keyframes onboarding-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes onboarding-out {
from { opacity: 1; }
to { opacity: 0; }
}
.onboarding-card {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-xl);
padding: var(--space-8);
max-width: 360px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
text-align: center;
box-shadow: var(--shadow-lg);
}
.onboarding-icon {
width: 48px;
height: 48px;
color: var(--color-accent);
}
.onboarding-title {
font-size: var(--text-xl);
font-weight: var(--font-weight-semibold);
color: var(--color-text);
margin: 0;
}
.onboarding-body {
font-size: var(--text-base);
color: var(--color-text-secondary);
line-height: 1.6;
margin: 0;
}
.onboarding-dots {
display: flex;
gap: var(--space-2);
}
.onboarding-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-border);
transition: background 0.2s;
}
.onboarding-dot--active {
background: var(--color-accent);
}
.onboarding-actions {
display: flex;
gap: var(--space-3);
width: 100%;
justify-content: flex-end;
margin-top: var(--space-2);
}
@media (prefers-reduced-motion: reduce) {
.onboarding-overlay,
.onboarding-overlay--out { animation: none; }
.onboarding-dot { transition: none; }
}