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);
+}