collab attachments, multi-day events, calendar improvements

- Collab buckets sync attachments alongside cards (images/PDFs/audio).
  New /api/collab-attachments endpoint on relay, encrypted at rest,
  bucket-scoped for members.
- CalDAV multi-day events via **Bis:** body field. Calendar renders
  event on every day in range with day counter (1/6, 2/6, ...).
- Calendar preserves month across close/reopen (only jumps to today
  on first open).
- Calendar title: centered nav group with [MM-DD] Mon Y [KW-KW] —
  dimmed brackets, fixed width so nothing wiggles.
- Editor: scratchpad uses custom confirm, autobackup-draft dialog
  replaced with custom styled dialog.
- Scanlines theme feature removed (performance issue).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-14 18:22:02 +02:00
1 parent 002562b1d3
commit ca47052768
11 files changed
+254 -64

No files matched your search

+51 -11
View File
@@ -88,20 +88,35 @@ function matchesRRule(card, targetDateStr) {
return false;
}
function getMultiDayEnd(card) {
const m = (card.body || '').match(/\*\*Bis:\*\*\s*(\d{4}-\d{2}-\d{2})/i);
return m ? m[1] : null;
}
function getCardsForDate(ds) {
const results = [];
const targetYear = parseInt(ds.substring(0, 4));
for (const c of state.cards) {
if (cardBaseDate(c) === ds) {
const base = cardBaseDate(c);
if (base === ds) {
results.push({ ...c, _recurring: false });
} else if (c.caldav_rrule && matchesRRule(c, ds)) {
const baseYear = parseInt(cardBaseDate(c).substring(0, 4));
} else if (base && base < ds) {
// Multi-day event: check if ds is within [base..end]
const end = getMultiDayEnd(c);
if (end && ds <= end) {
const startMs = Date.parse(base);
const curMs = Date.parse(ds);
const dayNum = Math.round((curMs - startMs) / 86400000) + 1;
const totalDays = Math.round((Date.parse(end) - startMs) / 86400000) + 1;
results.push({ ...c, _recurring: false, _multiDay: { day: dayNum, total: totalDays, isEnd: ds === end } });
}
}
if (c.caldav_rrule && matchesRRule(c, ds)) {
const baseYear = parseInt(base.substring(0, 4));
const rrule = parseRRule(c.caldav_rrule);
const age = rrule?.FREQ === 'YEARLY' ? targetYear - baseYear : null;
results.push({ ...c, _recurring: true, _age: age });
if (base !== ds) results.push({ ...c, _recurring: true, _age: age });
} else if ((c.tags || '').split(',').includes('geburtstag')) {
// Local birthday cards repeat yearly without RRULE
const base = cardBaseDate(c);
if (base.substring(5) === ds.substring(5) && ds !== base) {
const age = targetYear - parseInt(base.substring(0, 4));
results.push({ ...c, _recurring: true, _age: age > 0 ? age : null });
@@ -146,7 +161,25 @@ function clearCalHover() {
}
function render() {
title.textContent = `${MONTHS[state.calMonth]} ${state.calYear}`;
// Format: [MM-DD] Mon Y [KW-KW]
// MM-DD = today's month-day
// Mon Y = displayed month + year (abbreviated)
// KW-KW = ISO week range spanning the displayed month
const isoWeek = (y, m, d) => {
const dt = new Date(Date.UTC(y, m, d));
const dayNum = dt.getUTCDay() || 7;
dt.setUTCDate(dt.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
return Math.ceil((((dt - yearStart) / 86400000) + 1) / 7);
};
const pad = n => String(n).padStart(2, '0');
const nowDate = new Date();
const todayMd = `${pad(nowDate.getMonth() + 1)}-${pad(nowDate.getDate())}`;
const monAbbr = MONTHS[state.calMonth].substring(0, 3);
const lastDay = new Date(state.calYear, state.calMonth + 1, 0).getDate();
const wStart = isoWeek(state.calYear, state.calMonth, 1);
const wEnd = isoWeek(state.calYear, state.calMonth, lastDay);
title.innerHTML = `<span style="color:var(--t-dim)">[${todayMd}]</span> ${monAbbr} ${state.calYear} <span style="color:var(--t-dim)">[${pad(wStart)}-${pad(wEnd)}]</span>`;
const firstDay = new Date(state.calYear, state.calMonth, 1).getDay(); // 0=Sun
const startOffset = (firstDay + 6) % 7; // Mon=0
@@ -210,9 +243,11 @@ function render() {
const calCfg = getCardCalConfig(c);
const itemColor = calCfg?.color || c.color;
const itemIcon = calCfg?.icon || null;
const md = c._multiDay;
html += `<div class="cal-item" data-card-id="${c.id}" style="border-left-color:${itemColor};">`;
if (itemIcon) html += `<i class="ph ph-${itemIcon}" style="font-size:9px;color:${itemColor};flex-shrink:0;"></i>`;
if (timeStr) html += `<span style="font-family:var(--t-font-pixel);font-size:8px;color:var(--t-dim);flex-shrink:0;">${timeStr}</span>`;
if (md) html += `<span style="font-family:var(--t-font-pixel);font-size:8px;color:${itemColor};flex-shrink:0;">${md.day}/${md.total}</span>`;
else if (timeStr) html += `<span style="font-family:var(--t-font-pixel);font-size:8px;color:var(--t-dim);flex-shrink:0;">${timeStr}</span>`;
if (c._recurring && !itemIcon) html += `<i class="ph ph-arrows-clockwise" style="font-size:8px;color:var(--t-dim);flex-shrink:0;"></i>`;
html += `<span class="truncate">${esc(name)}</span>`;
html += `</div>`;
@@ -494,9 +529,14 @@ export function refreshCalendar() {
}
export function openCalendar() {
const now = new Date();
state.calYear = now.getFullYear();
state.calMonth = now.getMonth();
// Only jump to today on first open — preserve the user's navigation
// when the calendar is re-opened (e.g. after saving a card in a
// different month).
if (state.calYear == null || state.calMonth == null) {
const now = new Date();
state.calYear = now.getFullYear();
state.calMonth = now.getMonth();
}
render();
overlay.classList.add('open');
overlay.style.visibility = '';