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