diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index 472768b2..dba80e62 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -841,6 +841,75 @@ class FolkCalendarView extends HTMLElement { return Array.from(labels); } + /** + * Render day cell content — individual event labels when few events, + * spatial summary chips when events start to overlap. + */ + private renderDayCellContent(cellEvents: any[]): string { + // Few events: show individual labels (original behavior) + if (cellEvents.length <= 2) { + return cellEvents.map(e => { + const evColor = e.source_color || "#6366f1"; + const es = this.getEventStyles(e); + const city = this.getSpatialLabel(e.location_breadcrumb, 5); + const cityHtml = city ? `${this.esc(city)}` : ""; + const virtualHtml = e.is_virtual ? `\u{1F4BB}` : ""; + const likelihoodHtml = es.isTentative ? `${es.likelihoodLabel}` : ""; + return `
${e.rToolSource === "rSchedule" ? '🔔' : ""}${virtualHtml}${this.formatTime(e.start_time)}${this.esc(e.title)}${likelihoodHtml}${cityHtml}
`; + }).join(""); + } + + // Many events: semantic zoom — show spatial summary chips instead + // Group by location: count events per city, then decide label granularity + const located = cellEvents.filter(e => e.location_breadcrumb); + const virtualCount = cellEvents.filter(e => e.is_virtual).length; + + // Try city-level grouping first + const cityGroups = new Map(); + for (const e of located) { + const city = this.getSpatialLabel(e.location_breadcrumb, 5); + if (city) cityGroups.set(city, (cityGroups.get(city) || 0) + 1); + } + + // If too many cities (5+), zoom out to country level + let groups: Map; + let levelLabel: string; + if (cityGroups.size > 4) { + groups = new Map(); + for (const e of located) { + const country = this.getSpatialLabel(e.location_breadcrumb, 3); + if (country) groups.set(country, (groups.get(country) || 0) + 1); + } + levelLabel = "country"; + // If still too many countries, zoom to continent + if (groups.size > 4) { + groups = new Map(); + for (const e of located) { + const continent = this.getSpatialLabel(e.location_breadcrumb, 1); + if (continent) groups.set(continent, (groups.get(continent) || 0) + 1); + } + levelLabel = "continent"; + } + } else { + groups = cityGroups; + levelLabel = "city"; + } + + // Render spatial chips + let html = ""; + const sorted = Array.from(groups.entries()).sort((a, b) => b[1] - a[1]); + for (const [label, count] of sorted.slice(0, 3)) { + html += `
${this.esc(label)}${count}
`; + } + if (sorted.length > 3) { + html += `
+${sorted.length - 3}
`; + } + if (virtualCount > 0) { + html += `
\u{1F4BB} ${virtualCount}
`; + } + return html; + } + private toggleSource(name: string) { if (this.filteredSources.has(name)) { this.filteredSources.delete(name); } else { this.filteredSources.add(name); } @@ -1222,15 +1291,7 @@ class FolkCalendarView extends HTMLElement { ${dayReminders.slice(0, 3).map(r => ``).join("")} ${cellEvents.length > 5 ? `+${cellEvents.length - 5}` : ""} - ${cellEvents.slice(0, 2).map(e => { - const evColor = e.source_color || "#6366f1"; - const es = this.getEventStyles(e); - const city = this.getSpatialLabel(e.location_breadcrumb, 5); - const cityHtml = city ? `${this.esc(city)}` : ""; - const virtualHtml = e.is_virtual ? `\u{1F4BB}` : ""; - const likelihoodHtml = es.isTentative ? `${es.likelihoodLabel}` : ""; - return `
${e.rToolSource === "rSchedule" ? '🔔' : ""}${virtualHtml}${this.formatTime(e.start_time)}${this.esc(e.title)}${likelihoodHtml}${cityHtml}
`; - }).join("")} + ${this.renderDayCellContent(cellEvents)} ` : ""} `; @@ -2960,6 +3021,11 @@ class FolkCalendarView extends HTMLElement { .ev-time { color: var(--rs-text-muted); font-size: 8px; margin-right: 2px; } .ev-bell { margin-right: 2px; font-size: 8px; } .ev-loc { color: var(--rs-text-muted); font-size: 7px; margin-left: 3px; } + .ev-spatial-chip { display: flex; align-items: center; gap: 2px; font-size: 8px; color: var(--rs-text-secondary); background: var(--rs-bg-hover); border: 1px solid var(--rs-border); border-radius: 6px; padding: 1px 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; } + .ev-spatial-name { overflow: hidden; text-overflow: ellipsis; } + .ev-spatial-count { font-weight: 700; color: var(--rs-text-muted); font-size: 7px; flex-shrink: 0; } + .ev-spatial-more { color: var(--rs-text-muted); font-size: 7px; justify-content: center; } + .ev-spatial-virtual { color: var(--rs-text-muted); font-size: 7px; } .ev-virtual { font-size: 8px; margin-right: 2px; vertical-align: middle; } .ev-likelihood { font-size: 8px; color: var(--rs-warning); margin-left: 3px; } .dd-likelihood { font-size: 9px; color: var(--rs-warning); margin-left: 6px; } @@ -3250,6 +3316,7 @@ class FolkCalendarView extends HTMLElement { .day { min-height: 0; padding: 4px; } .day-num { font-size: 11px; } .ev-label { display: none; } + .ev-spatial-chip { display: none; } /* Dots → thin colored bars on mobile */ .dots { flex-direction: column; gap: 1px; margin-top: 2px; } .dot { width: 100%; height: 2px; border-radius: 1px; margin: 0; }