fix(ux): microinteraction fixes - swipe hint, locale loading, haptics, weather toast, FAB backdrop
- tasks.js: add maybeShowSwipeHint (long loop, max 3x) - matches shopping.js pattern - tasks.js: vibrate(15) on task status toggle - oikos-locale-picker: show disabled/loading state for both reload and setLocale paths - dashboard: show success toast after weather refresh (all 4 locales) - dashboard: add semi-transparent FAB backdrop to signal open mode
This commit is contained in:
@@ -54,9 +54,9 @@ class OikosLocalePicker extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select.addEventListener('change', () => {
|
select.addEventListener('change', () => {
|
||||||
|
select.disabled = true;
|
||||||
|
select.style.opacity = '0.5';
|
||||||
if (select.value === 'system') {
|
if (select.value === 'system') {
|
||||||
select.disabled = true;
|
|
||||||
select.style.opacity = '0.5';
|
|
||||||
localStorage.removeItem('oikos-locale');
|
localStorage.removeItem('oikos-locale');
|
||||||
// Kurze Verzögerung damit der Browser den disabled-Zustand rendert
|
// Kurze Verzögerung damit der Browser den disabled-Zustand rendert
|
||||||
setTimeout(() => location.reload(), 60);
|
setTimeout(() => location.reload(), 60);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"loadError": "Dashboard konnte nicht vollständig geladen werden.",
|
"loadError": "Dashboard konnte nicht vollständig geladen werden.",
|
||||||
"weatherRefresh": "Wetter aktualisieren",
|
"weatherRefresh": "Wetter aktualisieren",
|
||||||
"weatherRefreshTitle": "Aktualisieren",
|
"weatherRefreshTitle": "Aktualisieren",
|
||||||
|
"weatherUpdated": "Wetter aktualisiert",
|
||||||
"weatherFeelsLike": "Gefühlt {{temp}}° · {{humidity}}% · Wind {{wind}} km/h",
|
"weatherFeelsLike": "Gefühlt {{temp}}° · {{humidity}}% · Wind {{wind}} km/h",
|
||||||
"fabTaskLabel": "Aufgabe hinzufügen",
|
"fabTaskLabel": "Aufgabe hinzufügen",
|
||||||
"fabCalendarLabel": "Termin hinzufügen",
|
"fabCalendarLabel": "Termin hinzufügen",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"loadError": "Dashboard could not be fully loaded.",
|
"loadError": "Dashboard could not be fully loaded.",
|
||||||
"weatherRefresh": "Refresh weather",
|
"weatherRefresh": "Refresh weather",
|
||||||
"weatherRefreshTitle": "Refresh",
|
"weatherRefreshTitle": "Refresh",
|
||||||
|
"weatherUpdated": "Weather updated",
|
||||||
"weatherFeelsLike": "Feels like {{temp}}° · {{humidity}}% · Wind {{wind}} km/h",
|
"weatherFeelsLike": "Feels like {{temp}}° · {{humidity}}% · Wind {{wind}} km/h",
|
||||||
"fabTaskLabel": "Add task",
|
"fabTaskLabel": "Add task",
|
||||||
"fabCalendarLabel": "Add event",
|
"fabCalendarLabel": "Add event",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"loadError": "Impossibile caricare completamente la dashboard.",
|
"loadError": "Impossibile caricare completamente la dashboard.",
|
||||||
"weatherRefresh": "Aggiorna meteo",
|
"weatherRefresh": "Aggiorna meteo",
|
||||||
"weatherRefreshTitle": "Aggiorna",
|
"weatherRefreshTitle": "Aggiorna",
|
||||||
|
"weatherUpdated": "Meteo aggiornato",
|
||||||
"weatherFeelsLike": "Percepiti {{temp}}° · {{humidity}}% · Vento {{wind}} km/h",
|
"weatherFeelsLike": "Percepiti {{temp}}° · {{humidity}}% · Vento {{wind}} km/h",
|
||||||
"fabTaskLabel": "Aggiungi compito",
|
"fabTaskLabel": "Aggiungi compito",
|
||||||
"fabCalendarLabel": "Aggiungi evento",
|
"fabCalendarLabel": "Aggiungi evento",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"loadError": "Instrumentpanelen kunde inte laddas helt.",
|
"loadError": "Instrumentpanelen kunde inte laddas helt.",
|
||||||
"weatherRefresh": "Uppdatera vädret",
|
"weatherRefresh": "Uppdatera vädret",
|
||||||
"weatherRefreshTitle": "Uppdatera",
|
"weatherRefreshTitle": "Uppdatera",
|
||||||
|
"weatherUpdated": "Väder uppdaterat",
|
||||||
"weatherFeelsLike": "Känns som {{temp}}° · {{humidity}}% · Vind {{wind}} km/h",
|
"weatherFeelsLike": "Känns som {{temp}}° · {{humidity}}% · Vind {{wind}} km/h",
|
||||||
"fabTaskLabel": "Lägg till uppgift",
|
"fabTaskLabel": "Lägg till uppgift",
|
||||||
"fabCalendarLabel": "Lägg till händelse",
|
"fabCalendarLabel": "Lägg till händelse",
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ function renderFab() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
<div class="fab-backdrop" id="fab-backdrop"></div>
|
||||||
<div class="fab-container" id="fab-container">
|
<div class="fab-container" id="fab-container">
|
||||||
<button class="fab-main" id="fab-main" aria-label="${t('nav.quickActions')}" aria-expanded="false">
|
<button class="fab-main" id="fab-main" aria-label="${t('nav.quickActions')}" aria-expanded="false">
|
||||||
<i data-lucide="plus" aria-hidden="true"></i>
|
<i data-lucide="plus" aria-hidden="true"></i>
|
||||||
@@ -402,8 +403,9 @@ function renderFab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initFab(container, signal) {
|
function initFab(container, signal) {
|
||||||
const fabMain = container.querySelector('#fab-main');
|
const fabMain = container.querySelector('#fab-main');
|
||||||
const fabActions = container.querySelector('#fab-actions');
|
const fabActions = container.querySelector('#fab-actions');
|
||||||
|
const fabBackdrop = container.querySelector('#fab-backdrop');
|
||||||
if (!fabMain) return;
|
if (!fabMain) return;
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
@@ -414,6 +416,7 @@ function initFab(container, signal) {
|
|||||||
fabMain.setAttribute('aria-expanded', String(open));
|
fabMain.setAttribute('aria-expanded', String(open));
|
||||||
fabActions.classList.toggle('fab-actions--visible', open);
|
fabActions.classList.toggle('fab-actions--visible', open);
|
||||||
fabActions.setAttribute('aria-hidden', String(!open));
|
fabActions.setAttribute('aria-hidden', String(!open));
|
||||||
|
fabBackdrop?.classList.toggle('fab-backdrop--visible', open);
|
||||||
fabActions.querySelectorAll('[role="button"]').forEach((el) => {
|
fabActions.querySelectorAll('[role="button"]').forEach((el) => {
|
||||||
el.tabIndex = open ? 0 : -1;
|
el.tabIndex = open ? 0 : -1;
|
||||||
});
|
});
|
||||||
@@ -538,6 +541,7 @@ export async function render(container, { user }) {
|
|||||||
const newWidget = container.querySelector('#weather-widget');
|
const newWidget = container.querySelector('#weather-widget');
|
||||||
if (newWidget && window.lucide) window.lucide.createIcons({ el: newWidget });
|
if (newWidget && window.lucide) window.lucide.createIcons({ el: newWidget });
|
||||||
wireWeatherRefresh(container);
|
wireWeatherRefresh(container);
|
||||||
|
window.oikos?.showToast(t('dashboard.weatherUpdated'), 'success', 1500);
|
||||||
}
|
}
|
||||||
} catch { /* silently ignore */ }
|
} catch { /* silently ignore */ }
|
||||||
};
|
};
|
||||||
@@ -564,6 +568,7 @@ function wireWeatherRefresh(container) {
|
|||||||
const newWidget = container.querySelector('#weather-widget');
|
const newWidget = container.querySelector('#weather-widget');
|
||||||
if (newWidget && window.lucide) window.lucide.createIcons({ el: newWidget });
|
if (newWidget && window.lucide) window.lucide.createIcons({ el: newWidget });
|
||||||
wireWeatherRefresh(container);
|
wireWeatherRefresh(container);
|
||||||
|
window.oikos?.showToast(t('dashboard.weatherUpdated'), 'success', 1500);
|
||||||
}
|
}
|
||||||
} catch { /* silently ignore */ }
|
} catch { /* silently ignore */ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -656,6 +656,7 @@ function renderTaskList(container) {
|
|||||||
stagger(listEl.querySelectorAll('.swipe-row, .kanban-card'));
|
stagger(listEl.querySelectorAll('.swipe-row, .kanban-card'));
|
||||||
updateOverdueBadge();
|
updateOverdueBadge();
|
||||||
wireSwipeGestures(container);
|
wireSwipeGestures(container);
|
||||||
|
maybeShowSwipeHint(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFilters(container) {
|
function renderFilters(container) {
|
||||||
@@ -728,6 +729,9 @@ const SWIPE_THRESHOLD = 80; // px - Mindestweg für Aktion
|
|||||||
const SWIPE_MAX_VERT = 12; // px - vertikaler Bewegungs-Toleranzbereich (darunter: kein Scroll-Abbruch)
|
const SWIPE_MAX_VERT = 12; // px - vertikaler Bewegungs-Toleranzbereich (darunter: kein Scroll-Abbruch)
|
||||||
const SWIPE_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll (Swipe abgebrochen)
|
const SWIPE_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll (Swipe abgebrochen)
|
||||||
|
|
||||||
|
const SWIPE_HINT_KEY = 'oikos:swipeHintSeen';
|
||||||
|
const SWIPE_HINT_MAX = 3;
|
||||||
|
|
||||||
function wireSwipeGestures(container) {
|
function wireSwipeGestures(container) {
|
||||||
const listEl = container.querySelector('#task-list');
|
const listEl = container.querySelector('#task-list');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
@@ -850,6 +854,27 @@ function wireSwipeGestures(container) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// Swipe-Affordance Hint (Long Loop)
|
||||||
|
// Zeigt den Nudge-Hinweis maximal 3x (gespeichert in localStorage).
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
function maybeShowSwipeHint(container) {
|
||||||
|
if (window.innerWidth >= 1024) return; // Desktop: Swipe nicht relevant
|
||||||
|
const count = parseInt(localStorage.getItem(SWIPE_HINT_KEY) ?? '0', 10);
|
||||||
|
if (count >= SWIPE_HINT_MAX) return;
|
||||||
|
|
||||||
|
const firstRow = container.querySelector('.swipe-row');
|
||||||
|
if (!firstRow) return;
|
||||||
|
|
||||||
|
firstRow.classList.add('swipe-row--hint');
|
||||||
|
firstRow.addEventListener('animationend', () => {
|
||||||
|
firstRow.classList.remove('swipe-row--hint');
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
localStorage.setItem(SWIPE_HINT_KEY, String(count + 1));
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Event-Verdrahtung
|
// Event-Verdrahtung
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -921,6 +946,7 @@ function wireTaskList(container) {
|
|||||||
|
|
||||||
if (action === 'toggle-status') {
|
if (action === 'toggle-status') {
|
||||||
const status = target.dataset.status;
|
const status = target.dataset.status;
|
||||||
|
vibrate(15);
|
||||||
target.classList.toggle('task-status-btn--done', status !== 'done');
|
target.classList.toggle('task-status-btn--done', status !== 'done');
|
||||||
target.closest('.task-card')?.classList.toggle('task-card--done', status !== 'done');
|
target.closest('.task-card')?.classList.toggle('task-card--done', status !== 'done');
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -936,6 +936,21 @@
|
|||||||
background-color: var(--neutral-600);
|
background-color: var(--neutral-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fab-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
z-index: calc(var(--z-nav) - 10);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-backdrop--visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.fab-actions {
|
.fab-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user