diff --git a/frontend/css/app.css b/frontend/css/app.css
index 4f9797f..89e567b 100644
--- a/frontend/css/app.css
+++ b/frontend/css/app.css
@@ -469,8 +469,7 @@ a { color: var(--primary); text-decoration: none; }
letter-spacing: .5px; color: var(--text-2);
}
.month-kw-cell {
- position: absolute; left: 0; top: 0; bottom: 0;
- width: 38px;
+ position: absolute; left: 0; top: 0; bottom: 0; width: 38px;
display: flex; align-items: flex-start; justify-content: center;
padding-top: 6px;
font-size: 13px; color: var(--text-3); font-weight: 700;
@@ -478,32 +477,34 @@ a { color: var(--primary); text-decoration: none; }
cursor: default; user-select: none; z-index: 1;
background: var(--bg-app);
}
-.month-cell {
- flex: 1;
- border-right: 1px solid var(--border);
- padding: 4px 4px 0;
- cursor: pointer;
- transition: background var(--transition);
- min-width: 0;
+/* Full-height column divs — click target + border */
+.month-col {
+ flex: 1; border-right: 1px solid var(--border);
+ cursor: pointer; transition: background var(--transition);
+ padding: 4px 4px 0; min-width: 0;
}
-.month-cell:last-child { border-right: none; }
-.month-cell:hover { background: var(--bg-hover); }
-.month-cell.today { background: rgba(66,133,244,.08); }
-.month-cell.other-month .cell-day { color: var(--text-3); }
+.month-col:last-child { border-right: none; }
+.month-col:hover { background: var(--bg-hover); }
+.month-col.today { background: rgba(66,133,244,.08); }
+.month-col.other-month .cell-day { color: var(--text-3); }
.cell-day {
font-size: 12px; font-weight: 500; color: var(--text-2);
width: 26px; height: 26px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%; flex-shrink: 0;
}
-.cell-day.today {
- background: var(--today-color);
- color: #fff; font-weight: 700;
+.cell-day.today { background: var(--today-color); color: #fff; font-weight: 700; }
+/* Events overlay — pointer-events:none so clicks pass to columns */
+.month-events-overlay {
+ position: absolute; top: 30px; left: 0; right: 0; bottom: 0;
+ pointer-events: none; overflow: hidden; z-index: 2;
}
.month-more {
position: absolute;
font-size: 11px; color: var(--text-2); padding: 0 4px;
cursor: pointer; font-weight: 500;
+ pointer-events: all;
+ white-space: nowrap; overflow: hidden;
}
.month-more:hover { color: var(--primary); }
@@ -901,32 +902,24 @@ a { color: var(--primary); text-decoration: none; }
display: flex; flex-direction: column; flex: 1; overflow: hidden;
}
.month-row {
- display: flex; flex: 1; position: relative; min-height: 0;
+ display: flex; flex: 1; position: relative; min-height: 100px;
border-bottom: 1px solid var(--border);
}
.month-row:last-child { border-bottom: none; }
+/* row-right: flex row containing 7 full-height column divs + events overlay */
.month-row-right {
- margin-left: 38px; display: flex; flex-direction: column; flex: 1; min-width: 0;
-}
-.month-day-strip {
- display: flex; flex-shrink: 0;
-}
-.month-events-area {
- position: relative; flex: 1;
- min-height: 72px; /* 3 lanes × 22px + 6px padding */
- overflow: hidden;
+ margin-left: 38px; display: flex; flex: 1; position: relative; min-width: 0;
}
.month-span-event {
position: absolute;
height: 18px; line-height: 18px;
- border-radius: 3px;
- padding: 0 6px;
+ border-radius: 3px; padding: 0 6px;
font-size: 11px; font-weight: 500;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
cursor: pointer; color: #fff;
transition: filter var(--transition);
box-sizing: border-box;
- z-index: 2;
+ pointer-events: all;
}
.month-span-event:hover { filter: brightness(1.15); }
.month-span-event.past { opacity: .45; }
@@ -947,3 +940,118 @@ a { color: var(--primary); text-decoration: none; }
.logo-text { display: none; }
.view-title { font-size: 16px; }
}
+
+/* ── Custom Date/Time Display Field ─────────────────────── */
+.dt-display {
+ display: flex; align-items: center; justify-content: space-between;
+ background: var(--bg-app);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 10px 12px;
+ color: var(--text-1);
+ cursor: pointer;
+ transition: border-color var(--transition);
+ user-select: none;
+ min-height: 42px;
+}
+.dt-display:hover, .dt-display:focus { border-color: var(--primary); outline: none; }
+.dt-display-text { font-size: 14px; }
+.dt-display-icon { color: var(--text-3); flex-shrink: 0; margin-left: 6px; }
+
+/* ── Date/Time Picker Card ───────────────────────────────── */
+.dtp-overlay {
+ position: fixed; inset: 0; z-index: 700;
+}
+.dtp-card {
+ position: fixed; z-index: 701;
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow-lg);
+ padding: 16px;
+ width: 272px;
+ user-select: none;
+}
+/* Calendar header */
+.dtp-cal-header {
+ display: flex; align-items: center; justify-content: space-between;
+ margin-bottom: 10px;
+}
+.dtp-month-label {
+ font-size: 14px; font-weight: 600; color: var(--text-1);
+}
+.dtp-nav-btn {
+ background: none; border: none; cursor: pointer;
+ color: var(--text-2); font-size: 22px; line-height: 1;
+ padding: 2px 6px; border-radius: var(--radius-sm);
+ transition: background var(--transition), color var(--transition);
+}
+.dtp-nav-btn:hover { background: var(--bg-hover); color: var(--text-1); }
+/* Day-of-week headers */
+.dtp-grid {
+ display: grid; grid-template-columns: repeat(7, 1fr);
+ gap: 2px; margin-bottom: 4px;
+}
+.dtp-dow {
+ text-align: center; font-size: 11px; font-weight: 600;
+ color: var(--text-3); padding: 4px 0; text-transform: uppercase;
+}
+/* Day cells */
+.dtp-day {
+ display: flex; align-items: center; justify-content: center;
+ height: 34px; border-radius: 6px;
+ font-size: 13px; font-weight: 500; color: var(--text-1);
+ cursor: pointer; transition: background var(--transition);
+}
+.dtp-day:hover { background: var(--bg-hover); }
+.dtp-day.other { color: var(--text-3); }
+.dtp-day.other:hover { background: var(--bg-hover); }
+.dtp-day.today { color: var(--primary); font-weight: 700; }
+.dtp-day.selected {
+ background: var(--primary) !important;
+ color: #fff !important; font-weight: 700;
+}
+/* Time picker */
+.dtp-time-row {
+ display: flex; align-items: center; justify-content: center;
+ gap: 4px; margin: 12px 0 8px;
+ border-top: 1px solid var(--border-light);
+ padding-top: 12px;
+}
+.dtp-colon {
+ font-size: 22px; font-weight: 600; color: var(--text-2);
+ padding: 0 2px; line-height: 1;
+ align-self: center;
+}
+.dtp-tc-wrap {
+ position: relative; width: 64px;
+}
+.dtp-tc {
+ height: calc(3 * 40px); /* 120px = 3 visible items */
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ scrollbar-width: none;
+ padding: 40px 0; /* top/bottom padding so first/last can center */
+ box-sizing: content-box;
+}
+.dtp-tc::-webkit-scrollbar { display: none; }
+.dtp-ti {
+ height: 40px; line-height: 40px;
+ text-align: center; font-size: 20px; font-weight: 500;
+ scroll-snap-align: center;
+ border-radius: 8px;
+ cursor: pointer;
+ color: var(--text-2);
+ transition: background .1s, color .1s;
+}
+.dtp-ti:hover { color: var(--text-1); }
+.dtp-ti.selected {
+ background: var(--primary);
+ color: #fff;
+}
+/* Actions */
+.dtp-actions {
+ display: flex; justify-content: flex-end; gap: 8px;
+ margin-top: 12px; padding-top: 10px;
+ border-top: 1px solid var(--border-light);
+}
diff --git a/frontend/index.html b/frontend/index.html
index a489f43..480ebe7 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -202,21 +202,37 @@
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js
index 27f6fb4..4ddb9da 100644
--- a/frontend/js/calendar.js
+++ b/frontend/js/calendar.js
@@ -4,6 +4,7 @@ import { renderMonth } from './views/month.js';
import { renderWeek } from './views/week.js';
import { renderAgenda } from './views/agenda.js';
import { openColorPicker } from './color-picker.js';
+import { openDatePicker, formatDtDisplay } from './date-picker.js';
import { t, setLang, getLang } from './i18n.js';
// Fetch avatar image as blob URL (with auth header)
@@ -540,12 +541,13 @@ function bindTopbar() {
document.getElementById('btn-settings').onclick = openSettingsModal;
document.getElementById('btn-create-event').onclick = () => openNewEventModal(new Date());
- // Mouse wheel / trackpad scroll navigation
- let _wheelTimer = null;
+ // Mouse wheel / trackpad scroll navigation (500ms cooldown = 1 nav per gesture)
+ let _wheelLast = 0;
document.getElementById('view-container').addEventListener('wheel', e => {
e.preventDefault();
- if (_wheelTimer) return;
- _wheelTimer = setTimeout(() => { _wheelTimer = null; }, 80);
+ const now = Date.now();
+ if (now - _wheelLast < 500) return;
+ _wheelLast = now;
const dir = e.deltaY > 0 ? 1 : -1;
if (state.currentView === 'agenda') return;
if (state.currentView === 'month') {
@@ -710,6 +712,17 @@ function populateCalendarSelect(selectedId) {
});
}
+// ── Date field helpers ────────────────────────────────────
+function setDtValue(id, isoStr, mode) {
+ const input = document.getElementById(id);
+ if (input) input.value = isoStr || '';
+ const display = document.getElementById(id + '-display');
+ if (display) {
+ display.querySelector('.dt-display-text').textContent =
+ formatDtDisplay(isoStr, mode, getLang());
+ }
+}
+
function openNewEventModal(date) {
state.editingEvent = null;
state.selectedEventColor = '';
@@ -723,10 +736,10 @@ function openNewEventModal(date) {
const start = new Date(date);
const end = new Date(date);
end.setHours(end.getHours() + 1);
- document.getElementById('ev-start').value = toLocalDatetimeInput(start);
- document.getElementById('ev-end').value = toLocalDatetimeInput(end);
- document.getElementById('ev-start-date').value = toDateInput(start);
- document.getElementById('ev-end-date').value = toDateInput(start);
+ setDtValue('ev-start', toLocalDatetimeInput(start), 'datetime');
+ setDtValue('ev-end', toLocalDatetimeInput(end), 'datetime');
+ setDtValue('ev-start-date', toDateInput(start), 'date');
+ setDtValue('ev-end-date', toDateInput(start), 'date');
toggleAlldayFields(false);
populateCalendarSelect(null);
@@ -746,14 +759,14 @@ function openEditEventModal(ev) {
document.getElementById('ev-allday').checked = ev.allDay;
if (ev.allDay) {
- document.getElementById('ev-start-date').value = ev.start.slice(0, 10);
- document.getElementById('ev-end-date').value = ev.end.slice(0, 10);
+ setDtValue('ev-start-date', ev.start.slice(0, 10), 'date');
+ setDtValue('ev-end-date', ev.end.slice(0, 10), 'date');
toggleAlldayFields(true);
} else {
const s = new Date(ev.start);
const e = new Date(ev.end);
- document.getElementById('ev-start').value = toLocalDatetimeInput(s);
- document.getElementById('ev-end').value = toLocalDatetimeInput(e);
+ setDtValue('ev-start', toLocalDatetimeInput(s), 'datetime');
+ setDtValue('ev-end', toLocalDatetimeInput(e), 'datetime');
toggleAlldayFields(false);
}
@@ -781,6 +794,24 @@ function bindEventModal() {
toggleAlldayFields(e.target.checked);
});
+ // Date/time pickers
+ [
+ { displayId: 'ev-start-display', inputId: 'ev-start', mode: 'datetime' },
+ { displayId: 'ev-end-display', inputId: 'ev-end', mode: 'datetime' },
+ { displayId: 'ev-start-date-display', inputId: 'ev-start-date', mode: 'date' },
+ { displayId: 'ev-end-date-display', inputId: 'ev-end-date', mode: 'date' },
+ ].forEach(({ displayId, inputId, mode }) => {
+ const disp = document.getElementById(displayId);
+ if (!disp) return;
+ const open = async () => {
+ const current = document.getElementById(inputId)?.value || '';
+ const result = await openDatePicker(disp, current, mode);
+ if (result !== null) setDtValue(inputId, result, mode);
+ };
+ disp.addEventListener('click', open);
+ disp.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') open(); });
+ });
+
// Color picker: click preview to open gradient picker
const evColorPreview = document.getElementById('ev-color-preview');
const evColorHex = document.getElementById('ev-color-hex');
diff --git a/frontend/js/date-picker.js b/frontend/js/date-picker.js
new file mode 100644
index 0000000..80678dd
--- /dev/null
+++ b/frontend/js/date-picker.js
@@ -0,0 +1,271 @@
+/**
+ * Custom dark date/time picker
+ * openDatePicker(anchor, value, mode) → Promise
+ * anchor : DOM element to position near
+ * value : ISO string ("YYYY-MM-DDTHH:MM" | "YYYY-MM-DD") or ""
+ * mode : 'datetime' | 'date'
+ */
+import { t } from './i18n.js';
+
+const ITEM_H = 40; // px per scroll item
+const VISIBLE = 3; // visible items in time scroller
+
+export function openDatePicker(anchor, value, mode = 'datetime') {
+ return new Promise(resolve => {
+ // ── Parse initial value ───────────────────────────────
+ let selDate = new Date();
+ selDate.setHours(0, 0, 0, 0);
+ let selHour = selDate.getHours();
+ let selMin = 0;
+
+ if (value) {
+ try {
+ const raw = mode === 'datetime'
+ ? value.replace(' ', 'T')
+ : value + 'T00:00:00';
+ const d = new Date(raw);
+ if (!isNaN(d)) {
+ selDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ if (mode === 'datetime') { selHour = d.getHours(); selMin = d.getMinutes(); }
+ }
+ } catch (_) {}
+ }
+
+ let viewYear = selDate.getFullYear();
+ let viewMonth = selDate.getMonth();
+
+ // ── Build DOM ─────────────────────────────────────────
+ const overlay = document.createElement('div');
+ overlay.className = 'dtp-overlay';
+ document.body.appendChild(overlay);
+
+ const card = document.createElement('div');
+ card.className = 'dtp-card';
+ overlay.appendChild(card);
+
+ function done(result) {
+ overlay.remove();
+ resolve(result);
+ }
+
+ // Click outside → cancel
+ overlay.addEventListener('mousedown', e => {
+ if (e.target === overlay) done(null);
+ });
+
+ // ── Calendar builder ──────────────────────────────────
+ function buildCalendar() {
+ const months = t('months');
+ const dowKeys = t('dow_monday'); // always Monday-first in calendar
+
+ const firstDay = new Date(viewYear, viewMonth, 1);
+ const gridStart = new Date(firstDay);
+ let dow = firstDay.getDay();
+ dow = dow === 0 ? 6 : dow - 1; // 0=Mon…6=Sun
+ gridStart.setDate(gridStart.getDate() - dow);
+
+ const cells = [];
+ const iter = new Date(gridStart);
+ for (let i = 0; i < 42; i++) {
+ cells.push(new Date(iter));
+ iter.setDate(iter.getDate() + 1);
+ }
+
+ const today = new Date(); today.setHours(0, 0, 0, 0);
+
+ const dowHtml = dowKeys.map(d => `${d}
`).join('');
+ const daysHtml = cells.map(cell => {
+ const isOther = cell.getMonth() !== viewMonth;
+ const isToday = cell.getTime() === today.getTime();
+ const isSelected = cell.getTime() === selDate.getTime();
+ let cls = 'dtp-day';
+ if (isOther) cls += ' other';
+ if (isToday) cls += ' today';
+ if (isSelected) cls += ' selected';
+ return `${cell.getDate()}
`;
+ }).join('');
+
+ return `
+
+ ${dowHtml}
+ ${daysHtml}
+
`;
+ }
+
+ // ── Time scroll builder ───────────────────────────────
+ function buildTime() {
+ if (mode !== 'datetime') return '';
+ const hItems = Array.from({ length: 24 }, (_, i) =>
+ `${String(i).padStart(2,'0')}
`
+ ).join('');
+ const mItems = Array.from({ length: 60 }, (_, i) =>
+ `${String(i).padStart(2,'0')}
`
+ ).join('');
+ return ``;
+ }
+
+ // ── Render ────────────────────────────────────────────
+ function render() {
+ card.innerHTML =
+ buildCalendar() +
+ buildTime() +
+ `
+
+
+
`;
+
+ bindEvents();
+ if (mode === 'datetime') initScrollers();
+ positionCard();
+ }
+
+ // ── Event bindings ────────────────────────────────────
+ function bindEvents() {
+ card.querySelector('#dtp-prev').onclick = () => {
+ viewMonth--;
+ if (viewMonth < 0) { viewMonth = 11; viewYear--; }
+ render();
+ };
+ card.querySelector('#dtp-next').onclick = () => {
+ viewMonth++;
+ if (viewMonth > 11) { viewMonth = 0; viewYear++; }
+ render();
+ };
+
+ // Day click
+ card.querySelectorAll('.dtp-day').forEach(el => {
+ el.addEventListener('click', () => {
+ selDate = new Date(parseInt(el.dataset.ts));
+ if (el.classList.contains('other')) {
+ viewYear = selDate.getFullYear();
+ viewMonth = selDate.getMonth();
+ }
+ render();
+ });
+ });
+
+ card.querySelector('#dtp-cancel').onclick = () => done(null);
+ card.querySelector('#dtp-ok').onclick = () => done(buildResult());
+ }
+
+ // ── Time scroll initialisation ────────────────────────
+ function initScrollers() {
+ const hCol = card.querySelector('#dtp-h');
+ const mCol = card.querySelector('#dtp-m');
+ if (!hCol || !mCol) return;
+
+ // Scroll to selected value (padding-top = ITEM_H, so scrollTop = val * ITEM_H)
+ hCol.scrollTop = selHour * ITEM_H;
+ mCol.scrollTop = selMin * ITEM_H;
+ highlightItems(hCol, selHour);
+ highlightItems(mCol, selMin);
+
+ let hTimer, mTimer;
+
+ hCol.addEventListener('scroll', () => {
+ clearTimeout(hTimer);
+ hTimer = setTimeout(() => {
+ selHour = Math.max(0, Math.min(23, Math.round(hCol.scrollTop / ITEM_H)));
+ hCol.scrollTop = selHour * ITEM_H;
+ highlightItems(hCol, selHour);
+ }, 80);
+ });
+
+ mCol.addEventListener('scroll', () => {
+ clearTimeout(mTimer);
+ mTimer = setTimeout(() => {
+ selMin = Math.max(0, Math.min(59, Math.round(mCol.scrollTop / ITEM_H)));
+ mCol.scrollTop = selMin * ITEM_H;
+ highlightItems(mCol, selMin);
+ }, 80);
+ });
+
+ // Click item to select
+ hCol.querySelectorAll('.dtp-ti').forEach((el, i) => {
+ el.addEventListener('click', () => {
+ selHour = i;
+ hCol.scrollTo({ top: i * ITEM_H, behavior: 'smooth' });
+ highlightItems(hCol, i);
+ });
+ });
+ mCol.querySelectorAll('.dtp-ti').forEach((el, i) => {
+ el.addEventListener('click', () => {
+ selMin = i;
+ mCol.scrollTo({ top: i * ITEM_H, behavior: 'smooth' });
+ highlightItems(mCol, i);
+ });
+ });
+ }
+
+ function highlightItems(col, val) {
+ col.querySelectorAll('.dtp-ti').forEach((el, i) => {
+ el.classList.toggle('selected', i === val);
+ });
+ }
+
+ // ── Result builder ────────────────────────────────────
+ function buildResult() {
+ const y = selDate.getFullYear();
+ const mo = String(selDate.getMonth() + 1).padStart(2, '0');
+ const dd = String(selDate.getDate()).padStart(2, '0');
+ if (mode === 'date') return `${y}-${mo}-${dd}`;
+ const h = String(selHour).padStart(2, '0');
+ const mi = String(selMin).padStart(2, '0');
+ return `${y}-${mo}-${dd}T${h}:${mi}`;
+ }
+
+ // ── Positioning ───────────────────────────────────────
+ function positionCard() {
+ const r = anchor.getBoundingClientRect();
+ const cw = card.offsetWidth || 280;
+ const ch = card.offsetHeight || 420;
+ let left = r.left;
+ let top = r.bottom + 6;
+ if (left + cw > window.innerWidth - 8) left = window.innerWidth - cw - 8;
+ if (top + ch > window.innerHeight - 8) top = r.top - ch - 6;
+ if (left < 8) left = 8;
+ if (top < 8) top = 8;
+ card.style.left = left + 'px';
+ card.style.top = top + 'px';
+ }
+
+ render();
+ });
+}
+
+/**
+ * Format an ISO value for display in the UI
+ * mode: 'datetime' | 'date'
+ * lang: 'de' | 'en'
+ */
+export function formatDtDisplay(isoStr, mode, lang = 'de') {
+ if (!isoStr) return '—';
+ try {
+ const d = mode === 'datetime'
+ ? new Date(isoStr.replace(' ', 'T'))
+ : new Date(isoStr + 'T00:00:00');
+ if (isNaN(d)) return isoStr;
+ const locale = lang === 'en' ? 'en-GB' : 'de-CH';
+ if (mode === 'datetime') {
+ return d.toLocaleString(locale, {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ hour: '2-digit', minute: '2-digit', hour12: false,
+ });
+ }
+ return d.toLocaleDateString(locale, {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ });
+ } catch (_) { return isoStr; }
+}
diff --git a/frontend/js/views/month.js b/frontend/js/views/month.js
index 9775537..1d9de77 100644
--- a/frontend/js/views/month.js
+++ b/frontend/js/views/month.js
@@ -1,8 +1,8 @@
import { isToday, isPast, dayOfWeek, getISOWeekNumber } from '../utils.js';
import { t } from '../i18n.js';
-const LANE_H = 20; // px per lane (height 18px + 2px gap)
-const MAX_LANES = 3; // max visible event lanes per row
+const LANE_H = 20; // px per lane (event height 18px + 2px gap)
+const MAX_LANES = 3; // max visible lanes per row
export function renderMonth(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
const year = currentDate.getFullYear();
@@ -24,10 +24,8 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
// Normalize each event's date range once
const normed = events.map(ev => {
- const s = new Date(ev.start);
- s.setHours(0, 0, 0, 0);
- const e = new Date(ev.end);
- e.setHours(0, 0, 0, 0);
+ const s = new Date(ev.start); s.setHours(0, 0, 0, 0);
+ const e = new Date(ev.end); e.setHours(0, 0, 0, 0);
if (ev.allDay && e > s) e.setDate(e.getDate() - 1); // exclusive → inclusive
return { ev, ns: s, ne: e };
});
@@ -52,11 +50,10 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
const colStart = Math.max(0, daysBetween(rowStart, ns));
const colEnd = Math.min(6, daysBetween(rowStart, ne));
if (colEnd < colStart) return;
- const span = colEnd - colStart + 1;
rowItems.push({
ev,
colStart,
- span,
+ span: colEnd - colStart + 1,
continuesLeft: ns < rowStart,
continuesRight: ne > rowEnd,
});
@@ -71,7 +68,7 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
});
// Assign lanes (greedy interval packing)
- const lanes = []; // { colEnd }
+ const lanes = [];
rowItems.forEach(item => {
let laneIdx = lanes.findIndex(l => item.colStart >= l.colEnd);
if (laneIdx === -1) { laneIdx = lanes.length; lanes.push({ colEnd: 0 }); }
@@ -89,7 +86,7 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
}
});
- // Render event spans
+ // Render event spans HTML (placed in overlay)
let eventsHtml = '';
rowItems.forEach(item => {
if (item.lane >= MAX_LANES) return;
@@ -110,25 +107,23 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
title="${escAttr(ev.title)}">${escHtml(label)} `;
});
- // Render "+N more" per column
+ // "+N more" per column
Object.entries(overflowByCol).forEach(([col, count]) => {
const c = parseInt(col);
- const leftPct = (c / 7) * 100;
- const widthPct = (1 / 7) * 100;
eventsHtml += `${t('more_events', { n: count })}
`;
+ style="left:${((c / 7) * 100).toFixed(3)}%;width:${(100 / 7).toFixed(3)}%;bottom:2px">${t('more_events', { n: count })}`;
});
- // Day cells (numbers only)
- let dayCellsHtml = '';
+ // Full-height column divs (click targets + borders)
+ let colsHtml = '';
rowCells.forEach(cell => {
const key = dateKey(cell);
const isOther = cell.getMonth() !== month;
const todayCls = isToday(cell) ? 'today' : '';
const otherCls = isOther ? 'other-month' : '';
const numCls = isToday(cell) ? 'today' : '';
- dayCellsHtml += `
+ colsHtml += `
`;
});
@@ -136,8 +131,8 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
bodyHtml += `
${kw}
-
${dayCellsHtml}
-
${eventsHtml}
+ ${colsHtml}
+
${eventsHtml}
`;
}
@@ -147,7 +142,7 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
${bodyHtml}
`;
- // Click handlers — event delegation
+ // Click handlers via event delegation on the body
const body = container.querySelector('.month-body');
body.addEventListener('click', e => {
// Span event click
@@ -158,17 +153,17 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
if (ev) onEventClick(ev, spanEl);
return;
}
- // "+N more" click → day view
+ // "+N more" → navigate to day view
const moreEl = e.target.closest('.month-more');
if (moreEl) {
e.stopPropagation();
onDayClick(new Date(moreEl.dataset.date + 'T00:00:00'));
return;
}
- // Day cell click → day view
- const cellEl = e.target.closest('.month-cell');
- if (cellEl) {
- onDayClick(new Date(cellEl.dataset.date + 'T00:00:00'));
+ // Column click → navigate to day view
+ const colEl = e.target.closest('.month-col');
+ if (colEl) {
+ onDayClick(new Date(colEl.dataset.date + 'T00:00:00'));
}
});
}
@@ -176,7 +171,6 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
// ── Helpers ───────────────────────────────────────────────
function daysBetween(a, b) {
- // Number of whole days from date a to date b (can be negative)
return Math.round((b - a) / 86400000);
}