diff --git a/frontend/css/app.css b/frontend/css/app.css index 4a2c50c..04b2b66 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -733,6 +733,43 @@ a { color: var(--primary); text-decoration: none; } } .month-more:hover { color: var(--primary); } +/* Overflow popup ("+N weitere") */ +.month-overflow-popup { + position: fixed; z-index: 800; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: var(--shadow-lg); + padding: 10px 0; + min-width: 220px; max-width: 300px; + user-select: none; +} +.mop-header { + font-size: 12px; font-weight: 600; + color: var(--text-2); + padding: 0 12px 8px; + border-bottom: 1px solid var(--border); + margin-bottom: 4px; +} +.mop-row { + display: flex; align-items: center; gap: 7px; + padding: 4px 12px; cursor: pointer; + border-radius: 6px; margin: 0 4px; +} +.mop-row:hover { background: var(--bg-hover); } +.mop-dot { + width: 8px; height: 8px; border-radius: 50%; + flex-shrink: 0; +} +.mop-time { + font-size: 11px; color: var(--text-2); + min-width: 42px; flex-shrink: 0; +} +.mop-title { + font-size: 12px; font-weight: 500; color: var(--text-1); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} + /* ── Week / Day Views ───────────────────────────────────── */ .week-view, .day-view { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow-y: scroll; } .week-head-sticky { diff --git a/frontend/index.html b/frontend/index.html index 11810c8..97abaf7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -80,7 +80,7 @@ - + diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index bd8d437..20b1d21 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -9,8 +9,8 @@ import { openDatePicker, formatDtDisplay } from './date-picker.js'; import { t, setLang, getLang } from './i18n.js'; import { APP_VERSION } from './version.js'; -// Version sofort beim Modul-Load ueberall sichtbar setzen. -document.title = `Calendarr ${APP_VERSION}`; +// Version im Impressum/Sidebar sichtbar, nicht im Tab-Titel. +document.title = 'Calendarr'; document.addEventListener('DOMContentLoaded', () => { const imp = document.getElementById('impressum-version'); if (imp) imp.textContent = `Calendarr ${APP_VERSION}`; @@ -543,7 +543,7 @@ function updateTitle() { titleEl.innerHTML = `${main}` + (year ? `${year}` : ''); - document.title = `Calendarr ${APP_VERSION} - ${fullText}`; + document.title = `Calendarr – ${fullText}`; } function updateViewButtons() { diff --git a/frontend/js/version.js b/frontend/js/version.js index 769a8b9..c485baa 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 = 'v48'; +export const APP_VERSION = 'v49'; diff --git a/frontend/js/views/month.js b/frontend/js/views/month.js index 4e61fa0..dec482f 100644 --- a/frontend/js/views/month.js +++ b/frontend/js/views/month.js @@ -71,13 +71,21 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC return new Date(a.ev.start) - new Date(b.ev.start); }); - // Assign lanes (greedy interval packing) - const lanes = []; + // Assign lanes using a 2D occupancy grid (per-column tracking). + // This prevents gaps: an event in cols 3-6 no longer blocks col 0-2 in the same lane. + const occupiedGrid = []; // occupiedGrid[lane] = Set rowItems.forEach(item => { - let laneIdx = lanes.findIndex(l => item.colStart >= l.colEnd); - if (laneIdx === -1) { laneIdx = lanes.length; lanes.push({ colEnd: 0 }); } - item.lane = laneIdx; - lanes[laneIdx].colEnd = item.colStart + item.span; + const cols = Array.from({ length: item.span }, (_, i) => item.colStart + i); + let laneIdx = 0; + for (;;) { + if (!occupiedGrid[laneIdx]) occupiedGrid[laneIdx] = new Set(); + if (cols.every(c => !occupiedGrid[laneIdx].has(c))) { + cols.forEach(c => occupiedGrid[laneIdx].add(c)); + item.lane = laneIdx; + break; + } + laneIdx++; + } }); // Track overflow per column @@ -190,11 +198,20 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC if (ev) onEventClick(ev, spanEl); return; } - // "+N more" → navigate to day view + // "+N more" → show overflow popup with all events for that day const moreEl = e.target.closest('.month-more'); if (moreEl) { e.stopPropagation(); - onDayClick(new Date(moreEl.dataset.date + 'T00:00:00'), 'navigate'); + const dayDate = new Date(moreEl.dataset.date + 'T00:00:00'); + const dayEvents = normed + .filter(({ ns, ne }) => ns <= dayDate && ne >= dayDate) + .map(({ ev }) => ev) + .sort((a, b) => { + if (a.allDay && !b.allDay) return -1; + if (!a.allDay && b.allDay) return 1; + return new Date(a.start) - new Date(b.start); + }); + showOverflowPopup(moreEl, dayDate, dayEvents, onEventClick); return; } // Column click → select day @@ -243,3 +260,66 @@ function escHtml(s) { function escAttr(s) { return String(s).replace(/"/g,'"').replace(/'/g,'''); } + +function showOverflowPopup(anchor, date, events, onEventClick) { + document.querySelectorAll('.month-overflow-popup').forEach(p => p.remove()); + + const popup = document.createElement('div'); + popup.className = 'month-overflow-popup'; + + // Header: "Mo, 3. Jul" + const months = t('months'); + const dow = t('dow_monday'); + const dowIdx = (date.getDay() + 6) % 7; // 0=Mon + const header = document.createElement('div'); + header.className = 'mop-header'; + header.textContent = `${dow[dowIdx]}, ${date.getDate()}. ${months[date.getMonth()]}`; + popup.appendChild(header); + + events.forEach(ev => { + const row = document.createElement('div'); + row.className = 'mop-row'; + + const dot = document.createElement('span'); + dot.className = 'mop-dot'; + dot.style.background = ev.color || ev.calendarColor || '#4285f4'; + + const time = document.createElement('span'); + time.className = 'mop-time'; + time.textContent = ev.allDay ? t('allday_cap') : fmtTime(new Date(ev.start)); + + const title = document.createElement('span'); + title.className = 'mop-title'; + title.textContent = ev.title; + + row.append(dot, time, title); + row.addEventListener('click', e => { + e.stopPropagation(); + popup.remove(); + onEventClick(ev, row); + }); + popup.appendChild(row); + }); + + document.body.appendChild(popup); + + // Position near anchor, stay in viewport + const r = anchor.getBoundingClientRect(); + const pw = popup.offsetWidth || 260; + const ph = popup.offsetHeight || 200; + let left = r.left; + let top = r.bottom + 4; + if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8; + if (top + ph > window.innerHeight - 8) top = r.top - ph - 4; + if (left < 8) left = 8; + if (top < 8) top = 8; + popup.style.left = left + 'px'; + popup.style.top = top + 'px'; + + setTimeout(() => { + document.addEventListener('click', function close() { + popup.remove(); + document.removeEventListener('click', close); + }); + }, 0); +}