diff --git a/frontend/css/app.css b/frontend/css/app.css index 213afb0..6eefc88 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -1218,6 +1218,12 @@ a { color: var(--primary); text-decoration: none; } /* Backdrop element exists in DOM but is hidden by default on desktop */ .sidebar-backdrop { display: none; } +/* Mobile-only UI elements: hidden on desktop ───────────── */ +.view-mobile-wrapper { position: relative; display: none; } +.settings-nav-toggle { display: none; } +.settings-nav-backdrop { display: none; } +.dropdown-item-mobile-only { display: none; } + @media (max-width: 768px) { html, body { overflow-x: hidden; max-width: 100vw; } @@ -1291,16 +1297,14 @@ a { color: var(--primary); text-decoration: none; } .btn { min-height: 44px; } .view-switcher .view-btn { min-height: 40px; } - /* ── Month view: dots instead of full event titles ───────── */ + /* ── Month view: events keep text, just smaller ──────────── */ .month-span-event { - height: 6px !important; - line-height: 0 !important; - padding: 0 !important; - border-radius: 3px !important; - font-size: 0 !important; - text-overflow: clip !important; + height: 14px !important; + line-height: 14px !important; + padding: 0 4px !important; + font-size: 9px !important; + font-weight: 500; } - .month-events-overlay { gap: 1px; } .month-more { font-size: 9px; padding: 0 2px; @@ -1381,18 +1385,20 @@ a { color: var(--primary); text-decoration: none; } } .month-kw-cell { position: absolute; - left: 3px; top: 3px; + /* Bottom-left: away from the day-number (top-left) */ + left: 3px; bottom: 3px; + top: auto; right: auto; width: auto; height: auto; - bottom: auto; - padding: 1px 7px; + padding: 1px 6px; background: var(--bg-active); border: none !important; border-radius: 10px; - font-size: 10px; font-weight: 600; + font-size: 9px; font-weight: 600; color: var(--text-2); z-index: 5; display: inline-flex; align-items: center; + pointer-events: none; } /* ── Status-bar safe area inside full-screen modals (PWA) ── */ @@ -1404,6 +1410,64 @@ a { color: var(--primary); text-decoration: none; } .settings-page-body { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); } + + /* ── Topbar: hide desktop view-switcher + settings, show + mobile view-toggle and let the title breathe ──────────── */ + .topbar .view-switcher { display: none; } + .topbar #btn-settings { display: none; } + .view-mobile-wrapper { display: inline-block; } + .dropdown-item-mobile-only { display: flex; } + + /* Hide "Heute" button on desktop topbar (it's in the view popup now) */ + .topbar #btn-today { display: none; } + + /* The title is the most important info — let it grow */ + .topbar-center { flex: 1; min-width: 0; } + .topbar-center .view-title { + font-size: 17px; + font-weight: 500; + padding-left: 4px; + flex: 1; + } + .topbar-left { gap: 0; } + .topbar-right { gap: 0; } + + /* ── Settings modal: nav becomes a slide-in overlay ──────── */ + .settings-nav-toggle { display: inline-flex !important; } + .settings-page-body { position: relative; } + .settings-nav { + position: absolute; + top: 0; left: 0; bottom: 0; + width: min(75vw, 280px); + z-index: 50; + background: var(--bg-app); + border-right: 1px solid var(--border); + transform: translateX(-100%); + transition: transform .25s ease; + box-shadow: var(--shadow-lg); + } + .settings-page-card.nav-open .settings-nav { + transform: translateX(0); + } + .settings-nav-backdrop { + display: block; + position: absolute; inset: 0; + background: rgba(0,0,0,.5); + z-index: 40; + opacity: 0; + pointer-events: none; + transition: opacity .2s ease; + } + .settings-page-card.nav-open .settings-nav-backdrop { + opacity: 1; + pointer-events: auto; + } + .settings-panels { padding: 16px; } + + /* Modal headers: tighter on mobile */ + .modal-header { padding: 12px 16px; } + .modal-body { padding: 16px; } + .modal-footer { padding: 12px 16px; } } /* iOS notch / home-indicator safe areas (PWA standalone) */ diff --git a/frontend/index.html b/frontend/index.html index 9efd076..4a90364 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Calendarr v4 + Calendarr v5 @@ -77,7 +77,7 @@ - + @@ -112,6 +112,19 @@ +
+ + +
@@ -123,6 +136,10 @@ Profil + + @@ -534,10 +551,14 @@ +

Einstellungen

+
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 464aeda..85ace73 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -756,21 +756,57 @@ function renderCalendarList() { }); } -// ── Swipe navigation (mobile) ───────────────────────────── +// ── Swipe navigation + long-press → context menu (mobile) ── function bindSwipeNavigation() { const container = document.getElementById('view-container'); if (!container) return; let startX = 0, startY = 0, startT = 0, active = false; + let lpTimer = null, lpTarget = null, lpFired = false; + container.addEventListener('touchstart', e => { if (e.touches.length !== 1) { active = false; return; } startX = e.touches[0].clientX; startY = e.touches[0].clientY; startT = Date.now(); active = true; + lpFired = false; + + // Long-press → context menu (only on day cells, not on events) + lpTarget = e.target.closest('.month-col, .week-day-col'); + if (lpTarget && !e.target.closest('.month-span-event, .week-event')) { + lpTimer = setTimeout(() => { + const t = e.touches[0]; + const ev = new MouseEvent('contextmenu', { + bubbles: true, cancelable: true, + clientX: t.clientX, clientY: t.clientY, + }); + lpTarget.dispatchEvent(ev); + lpFired = true; + }, 500); + } }, { passive: true }); + + container.addEventListener('touchmove', e => { + if (!active) return; + const t = e.touches[0]; + if (Math.abs(t.clientX - startX) > 8 || Math.abs(t.clientY - startY) > 8) { + if (lpTimer) { clearTimeout(lpTimer); lpTimer = null; } + } + }, { passive: true }); + container.addEventListener('touchend', e => { + if (lpTimer) { clearTimeout(lpTimer); lpTimer = null; } if (!active) return; active = false; + + // Suppress the click that follows a long-press + if (lpFired) { + const blocker = ev => { ev.stopPropagation(); ev.preventDefault(); }; + document.addEventListener('click', blocker, { capture: true, once: true }); + lpFired = false; + return; + } + const t = e.changedTouches[0]; const dx = t.clientX - startX; const dy = t.clientY - startY; @@ -781,6 +817,11 @@ function bindSwipeNavigation() { fetchAndRender(); } }, { passive: true }); + + container.addEventListener('touchcancel', () => { + if (lpTimer) { clearTimeout(lpTimer); lpTimer = null; } + active = false; + }, { passive: true }); } // ── Navigation ──────────────────────────────────────────── @@ -825,6 +866,59 @@ function bindTopbar() { document.getElementById('btn-settings').onclick = openSettingsModal; document.getElementById('btn-create-event').onclick = () => openNewEventModal(state.selectedDate || state.currentDate); + // Mobile view-toggle popup + const viewMobileBtn = document.getElementById('btn-view-mobile'); + const viewMobileDropdown = document.getElementById('view-mobile-dropdown'); + if (viewMobileBtn && viewMobileDropdown) { + viewMobileBtn.onclick = e => { + e.stopPropagation(); + viewMobileDropdown.classList.toggle('hidden'); + }; + document.addEventListener('click', e => { + if (!viewMobileDropdown.contains(e.target) && !viewMobileBtn.contains(e.target)) { + viewMobileDropdown.classList.add('hidden'); + } + }); + viewMobileDropdown.querySelectorAll('[data-mobile-view]').forEach(btn => { + btn.onclick = () => { + state.currentView = btn.dataset.mobileView; + updateViewButtons(); + fetchAndRender(); + viewMobileDropdown.classList.add('hidden'); + }; + }); + const todayMobile = document.getElementById('btn-today-mobile'); + if (todayMobile) todayMobile.onclick = () => { + state.currentDate = new Date(); + fetchAndRender(); + viewMobileDropdown.classList.add('hidden'); + }; + } + + // Settings entry inside the user dropdown (mobile) + const settingsFromUser = document.getElementById('btn-settings-from-user'); + if (settingsFromUser) settingsFromUser.onclick = () => { + document.getElementById('user-dropdown').classList.add('hidden'); + openSettingsModal(); + }; + + // Settings nav hamburger (only does something on mobile via CSS) + const settingsNavToggle = document.getElementById('settings-nav-toggle'); + const settingsCard = document.querySelector('#modal-settings .settings-page-card'); + const settingsNavBackdrop = document.getElementById('settings-nav-backdrop'); + if (settingsNavToggle && settingsCard) { + settingsNavToggle.onclick = () => settingsCard.classList.toggle('nav-open'); + } + if (settingsNavBackdrop && settingsCard) { + settingsNavBackdrop.onclick = () => settingsCard.classList.remove('nav-open'); + } + // After picking a section in the nav, close the overlay (mobile UX) + document.querySelectorAll('.settings-nav-btn').forEach(btn => { + btn.addEventListener('click', () => { + if (settingsCard) settingsCard.classList.remove('nav-open'); + }); + }); + // Mouse wheel / trackpad scroll navigation – only for month & quarter let _wheelLast = 0; document.getElementById('view-container').addEventListener('wheel', e => { diff --git a/frontend/js/version.js b/frontend/js/version.js index 9f06a98..d5ed145 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v4'; +export const APP_VERSION = 'v5'; diff --git a/frontend/sw.js b/frontend/sw.js index 0213bb1..4783304 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,7 +1,7 @@ // Calendarr Service Worker // Cache-first for static assets, network-first for /api/* (graceful offline) -const CACHE_VERSION = 'calendarr-v4'; +const CACHE_VERSION = 'calendarr-v5'; const STATIC_ASSETS = [ '/', '/index.html',