feat: add colorful, spatially-enriched calendar event rendering
Progressive spatial detail across all zoom levels: stacked source-color bars in multi-year, multi-dot indicators in year views, city chips in season, colored bg tints with location labels in month/week/day views, description previews and virtual platform badges in day detail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7e5499d087
commit
e838cb9a7e
|
|
@ -456,6 +456,31 @@ class FolkCalendarView extends HTMLElement {
|
|||
&& !this.filteredSources.has(e.source_name));
|
||||
}
|
||||
|
||||
private getSpatialLabel(breadcrumb: string | null | undefined, level: number): string {
|
||||
if (!breadcrumb) return "";
|
||||
const parts = breadcrumb.split(" > ").map(s => s.trim());
|
||||
// SPATIAL_LABELS: Planet(0), Continent(1), Bioregion(2), Country(3), Region(4), City(5), Neighborhood(6), Address(7), Coordinates(8)
|
||||
// Breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" → indices 0,1,2,3,4
|
||||
// Map: Country(3)→idx 2, Region(4)→idx 3, City(5)→idx 3-4, Neighborhood(6)→idx 4
|
||||
const indexMap: Record<number, number> = { 0: 0, 1: 1, 2: 1, 3: 2, 4: 3, 5: 3, 6: 4, 7: 4, 8: 4 };
|
||||
const partIdx = indexMap[level] ?? 2;
|
||||
return parts[partIdx] || parts[parts.length - 1] || "";
|
||||
}
|
||||
|
||||
private getCurrentSpatialLabel(): string {
|
||||
return SPATIAL_LABELS[this.getEffectiveSpatialIndex()];
|
||||
}
|
||||
|
||||
private getUniqueSpatialLabels(events: any[], level: number, max: number): string[] {
|
||||
const labels = new Set<string>();
|
||||
for (const e of events) {
|
||||
if (labels.size >= max) break;
|
||||
const lbl = this.getSpatialLabel(e.location_breadcrumb, level);
|
||||
if (lbl) labels.add(lbl);
|
||||
}
|
||||
return Array.from(labels);
|
||||
}
|
||||
|
||||
private toggleSource(name: string) {
|
||||
if (this.filteredSources.has(name)) { this.filteredSources.delete(name); }
|
||||
else { this.filteredSources.add(name); }
|
||||
|
|
@ -784,12 +809,16 @@ class FolkCalendarView extends HTMLElement {
|
|||
</div>
|
||||
${dayEvents.length > 0 ? `
|
||||
<div class="dots">
|
||||
${dayEvents.slice(0, 4).map(e => `<span class="dot" style="background:${e.source_color || "#6366f1"}"></span>`).join("")}
|
||||
${dayEvents.length > 4 ? `<span style="font-size:8px;color:#888">+${dayEvents.length - 4}</span>` : ""}
|
||||
${dayEvents.slice(0, 5).map(e => `<span class="dot" style="background:${e.source_color || "#6366f1"}"></span>`).join("")}
|
||||
${dayEvents.length > 5 ? `<span style="font-size:8px;color:#888">+${dayEvents.length - 5}</span>` : ""}
|
||||
</div>
|
||||
${dayEvents.slice(0, 2).map(e =>
|
||||
`<div class="ev-label" style="border-left:2px solid ${e.source_color || "#6366f1"}" data-event-id="${e.id}">${e.rToolSource === "rSchedule" ? '<span class="ev-bell">🔔</span>' : ""}<span class="ev-time">${this.formatTime(e.start_time)}</span>${this.esc(e.title)}</div>`
|
||||
).join("")}
|
||||
${dayEvents.slice(0, 2).map(e => {
|
||||
const evColor = e.source_color || "#6366f1";
|
||||
const city = this.getSpatialLabel(e.location_breadcrumb, 5);
|
||||
const cityHtml = city ? `<span class="ev-loc">${this.esc(city)}</span>` : "";
|
||||
const virtualHtml = e.is_virtual ? `<span class="ev-virtual" title="Virtual">\u{1F4BB}</span>` : "";
|
||||
return `<div class="ev-label" style="border-left:2px solid ${evColor};background:${evColor}10" data-event-id="${e.id}">${e.rToolSource === "rSchedule" ? '<span class="ev-bell">🔔</span>' : ""}${virtualHtml}<span class="ev-time">${this.formatTime(e.start_time)}</span>${this.esc(e.title)}${cityHtml}</div>`;
|
||||
}).join("")}
|
||||
` : ""}
|
||||
</div>`;
|
||||
|
||||
|
|
@ -824,10 +853,24 @@ class FolkCalendarView extends HTMLElement {
|
|||
const months = [quarter * 3, quarter * 3 + 1, quarter * 3 + 2];
|
||||
const seasonName = ["Winter", "Spring", "Summer", "Autumn"][quarter];
|
||||
|
||||
const monthsHtml = months.map(m => {
|
||||
const dim = new Date(year, m + 1, 0).getDate();
|
||||
const monthEvents: any[] = [];
|
||||
for (let d = 1; d <= dim; d++) {
|
||||
const ds = `${year}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
||||
monthEvents.push(...this.getEventsForDate(ds));
|
||||
}
|
||||
const cities = this.getUniqueSpatialLabels(monthEvents, 5, 4);
|
||||
const citiesHtml = cities.length > 0
|
||||
? `<div class="season-cities">${cities.map(c => `<span class="season-city-chip">${this.esc(c)}</span>`).join("")}</div>`
|
||||
: "";
|
||||
return `<div class="season-month-wrap">${this.renderMiniMonth(year, m)}${citiesHtml}</div>`;
|
||||
}).join("");
|
||||
|
||||
return `
|
||||
<div class="season-header">${seasonName} ${year} <span class="season-q">Q${quarter + 1}</span></div>
|
||||
<div class="season-grid">
|
||||
${months.map(m => this.renderMiniMonth(year, m)).join("")}
|
||||
${monthsHtml}
|
||||
</div>
|
||||
<div class="mini-hint">Click any day to zoom in</div>`;
|
||||
}
|
||||
|
|
@ -875,12 +918,14 @@ class FolkCalendarView extends HTMLElement {
|
|||
const duration = Math.max(endMin - startMin, 30);
|
||||
const leftPx = ((startMin - START_HOUR * 60) / 60) * HOUR_WIDTH;
|
||||
const widthPx = Math.max((duration / 60) * HOUR_WIDTH, 60);
|
||||
const bgColor = ev.source_color ? `${ev.source_color}18` : "#6366f118";
|
||||
const bgColor = ev.source_color ? `${ev.source_color}24` : "#6366f124";
|
||||
const virtualBadge = ev.is_virtual ? `<span class="dh-virtual" title="Virtual">\u{1F4BB}</span>` : "";
|
||||
|
||||
eventsHtml += `<div class="dh-event" data-event-id="${ev.id}" style="
|
||||
left:${leftPx}px;width:${widthPx}px;background:${bgColor};border-top:3px solid ${ev.source_color || "#6366f1"}">
|
||||
<div class="tl-event-title">${this.esc(ev.title)}</div>
|
||||
<div class="tl-event-title">${virtualBadge}${this.esc(ev.title)}</div>
|
||||
<div class="tl-event-time">${this.formatTime(ev.start_time)} \u2013 ${this.formatTime(ev.end_time)}</div>
|
||||
${ev.location_name ? `<div class="dh-event-loc">${this.esc(ev.location_name)}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -945,9 +990,20 @@ class FolkCalendarView extends HTMLElement {
|
|||
const isToday = ds === todayStr;
|
||||
const dayEvents = this.getEventsForDate(ds);
|
||||
const isWeekend = dow === 0 || dow === 6;
|
||||
rowsHtml += `<div class="mt-cell ${isToday ? "today" : ""} ${isWeekend ? "weekend" : ""}" data-date="${ds}">
|
||||
const cities = this.getUniqueSpatialLabels(dayEvents, 5, 2);
|
||||
const mtTooltip = dayEvents.length > 0 ? `${dayEvents.length} event${dayEvents.length > 1 ? 's' : ''}${cities.length ? ' \u00B7 ' + cities.join(', ') : ''}` : '';
|
||||
// Stacked color bar
|
||||
let mtBarHtml = "";
|
||||
if (dayEvents.length > 0) {
|
||||
const mtColors: Record<string, number> = {};
|
||||
for (const e of dayEvents) { const c = e.source_color || "#6366f1"; mtColors[c] = (mtColors[c] || 0) + 1; }
|
||||
const segs = Object.entries(mtColors).map(([c, n]) => `<span class="mt-seg" style="background:${c};flex:${n}"></span>`).join("");
|
||||
mtBarHtml = `<span class="mt-color-bar">${segs}</span>`;
|
||||
}
|
||||
rowsHtml += `<div class="mt-cell ${isToday ? "today" : ""} ${isWeekend ? "weekend" : ""}" data-date="${ds}" title="${mtTooltip}">
|
||||
<span class="mt-num">${dayOfMonth}</span>
|
||||
${dayEvents.length > 0 ? `<span class="mt-count" style="background:${dayEvents[0].source_color || "#6366f1"}">${dayEvents.length}</span>` : ""}
|
||||
${mtBarHtml}
|
||||
${dayEvents.length > 0 ? `<span class="mt-count-num">${dayEvents.length}</span>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
rowsHtml += `</div>`;
|
||||
|
|
@ -971,22 +1027,35 @@ class FolkCalendarView extends HTMLElement {
|
|||
for (let m = 0; m < 12; m++) {
|
||||
const monthName = new Date(year, m, 1).toLocaleDateString("default", { month: "short" });
|
||||
const dim = new Date(year, m + 1, 0).getDate();
|
||||
const monthEvents: any[] = [];
|
||||
|
||||
let daysHtml = "";
|
||||
for (let d = 1; d <= dim; d++) {
|
||||
const ds = `${year}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
||||
const isToday = ds === todayStr;
|
||||
const dayEvents = this.getEventsForDate(ds);
|
||||
monthEvents.push(...dayEvents);
|
||||
const dow = new Date(year, m, d).getDay();
|
||||
const isWeekend = dow === 0 || dow === 6;
|
||||
|
||||
// Multi-dot support (up to 3)
|
||||
let dotsHtml = "";
|
||||
if (dayEvents.length > 0) {
|
||||
const colors = dayEvents.slice(0, 3).map(e => e.source_color || "#6366f1");
|
||||
dotsHtml = `<span class="yv-dots">${colors.map(c => `<span class="yv-dot" style="background:${c}"></span>`).join("")}</span>`;
|
||||
}
|
||||
|
||||
daysHtml += `<div class="yv-day ${isToday ? "today" : ""} ${isWeekend ? "weekend" : ""}" data-mini-date="${ds}" title="${d} ${monthName}${dayEvents.length ? ` (${dayEvents.length} events)` : ""}">
|
||||
${d}
|
||||
${dayEvents.length > 0 ? `<span class="yv-dot" style="background:${dayEvents[0].source_color || "#6366f1"}"></span>` : ""}
|
||||
${dotsHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const countries = this.getUniqueSpatialLabels(monthEvents, 3, 2);
|
||||
const countryHtml = countries.length > 0 ? `<span class="yv-country">${countries.join(", ")}</span>` : "";
|
||||
|
||||
html += `<div class="yv-month" data-mini-month="${m}">
|
||||
<div class="yv-label">${monthName}</div>
|
||||
<div class="yv-label">${monthName}${countryHtml}</div>
|
||||
<div class="yv-days">${daysHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -1022,18 +1091,37 @@ class FolkCalendarView extends HTMLElement {
|
|||
private renderMicroMonth(year: number, month: number): string {
|
||||
const monthInitials = ["J","F","M","A","M","J","J","A","S","O","N","D"];
|
||||
const dim = new Date(year, month + 1, 0).getDate();
|
||||
let eventCount = 0;
|
||||
const allEvents: any[] = [];
|
||||
for (let d = 1; d <= dim; d++) {
|
||||
const ds = `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
||||
eventCount += this.getEventsForDate(ds).length;
|
||||
allEvents.push(...this.getEventsForDate(ds));
|
||||
}
|
||||
const maxBar = 8; // normalize bar width
|
||||
const eventCount = allEvents.length;
|
||||
const maxBar = 8;
|
||||
const barWidth = Math.min(eventCount, maxBar);
|
||||
const barPct = (barWidth / maxBar) * 100;
|
||||
|
||||
return `<div class="micro-month" data-micro-click="${year}-${month}" title="${new Date(year, month, 1).toLocaleDateString("default", { month: "long", year: "numeric" })}${eventCount ? ` (${eventCount} events)` : ""}">
|
||||
// Group by source_color for stacked segments
|
||||
const colorCounts: Record<string, number> = {};
|
||||
for (const e of allEvents) {
|
||||
const c = e.source_color || "#6366f1";
|
||||
colorCounts[c] = (colorCounts[c] || 0) + 1;
|
||||
}
|
||||
const countries = this.getUniqueSpatialLabels(allEvents, 3, 3);
|
||||
const tooltipExtra = countries.length > 0 ? ` \u00B7 ${countries.join(", ")}` : "";
|
||||
|
||||
let barHtml = "";
|
||||
if (eventCount > 0) {
|
||||
const segments = Object.entries(colorCounts).map(([color, count]) => {
|
||||
const segPct = (count / eventCount) * 100;
|
||||
return `<span class="micro-seg" style="background:${color};width:${segPct}%"></span>`;
|
||||
}).join("");
|
||||
barHtml = `<span class="micro-bar-stack" style="width:${barPct}%">${segments}</span>`;
|
||||
}
|
||||
|
||||
return `<div class="micro-month" data-micro-click="${year}-${month}" title="${new Date(year, month, 1).toLocaleDateString("default", { month: "long", year: "numeric" })}${eventCount ? ` (${eventCount} events)${tooltipExtra}` : ""}">
|
||||
<span class="micro-label">${monthInitials[month]}</span>
|
||||
${eventCount > 0 ? `<span class="micro-bar" style="width:${barPct}%"></span>` : ""}
|
||||
${barHtml}
|
||||
${eventCount > 0 ? `<span class="micro-count">${eventCount}</span>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -1056,9 +1144,20 @@ class FolkCalendarView extends HTMLElement {
|
|||
const isToday = ds === todayStr;
|
||||
const dayEvents = this.getEventsForDate(ds);
|
||||
const hasEvents = dayEvents.length > 0;
|
||||
const isBusy = dayEvents.length >= 3;
|
||||
const titleParts = dayEvents.slice(0, 3).map(e => e.title);
|
||||
const tooltipText = hasEvents ? `${dayEvents.length} event${dayEvents.length > 1 ? 's' : ''}: ${titleParts.join(', ')}` : '';
|
||||
const busyStyle = isBusy ? `background:rgba(99,102,241,0.08);` : '';
|
||||
|
||||
// Up to 3 stacked dots with individual colors
|
||||
let dotsHtml = "";
|
||||
if (hasEvents) {
|
||||
const uniqueColors = dayEvents.slice(0, 3).map(e => e.source_color || '#6366f1');
|
||||
dotsHtml = `<span class="mini-dots">${uniqueColors.map(c => `<span class="mini-dot" style="background:${c}"></span>`).join("")}</span>`;
|
||||
}
|
||||
|
||||
daysHtml += `<div class="mini-day ${isToday ? "today" : ""}" data-mini-date="${ds}"
|
||||
title="${hasEvents ? dayEvents.length + ' event' + (dayEvents.length > 1 ? 's' : '') : ''}">${d}${hasEvents ? `<span class="mini-dot" style="background:${dayEvents[0].source_color || '#6366f1'}"></span>` : ""}</div>`;
|
||||
style="${busyStyle}" title="${tooltipText}">${d}${dotsHtml}</div>`;
|
||||
}
|
||||
|
||||
return `<div class="mini-month ${isCurrentMonth ? "current" : ""}" data-mini-month="${month}">
|
||||
|
|
@ -1104,13 +1203,19 @@ class FolkCalendarView extends HTMLElement {
|
|||
const duration = Math.max(endMin - startMin, 30);
|
||||
const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT;
|
||||
const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 24);
|
||||
const bgColor = ev.source_color ? `${ev.source_color}18` : "#6366f118";
|
||||
const bgColor = ev.source_color ? `${ev.source_color}20` : "#6366f120";
|
||||
const showDesc = heightPx >= 60 && ev.description;
|
||||
const descPreview = showDesc ? (ev.description.length > 60 ? ev.description.slice(0, 57) + "..." : ev.description) : "";
|
||||
const neighborhood = this.getSpatialLabel(ev.location_breadcrumb, 6);
|
||||
const virtualBadge = ev.is_virtual ? `<span class="tl-virtual" title="${this.esc(ev.virtual_platform || 'Virtual')}">\u{1F4BB} ${this.esc(ev.virtual_platform || 'Virtual')}</span>` : "";
|
||||
|
||||
eventsHtml += `<div class="tl-event" data-event-id="${ev.id}" style="
|
||||
top:${topPx}px;height:${heightPx}px;background:${bgColor};border-left-color:${ev.source_color || "#6366f1"}">
|
||||
<div class="tl-event-title">${this.esc(ev.title)}</div>
|
||||
<div class="tl-event-time">${this.formatTime(ev.start_time)} \u2013 ${this.formatTime(ev.end_time)}</div>
|
||||
<div class="tl-event-time">${this.formatTime(ev.start_time)} \u2013 ${this.formatTime(ev.end_time)}${virtualBadge}</div>
|
||||
${ev.location_name ? `<div class="tl-event-loc">${this.esc(ev.location_name)}</div>` : ""}
|
||||
${neighborhood ? `<span class="tl-breadcrumb">${this.esc(neighborhood)}</span>` : ""}
|
||||
${showDesc ? `<div class="tl-event-desc">${this.esc(descPreview)}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1187,13 +1292,18 @@ class FolkCalendarView extends HTMLElement {
|
|||
const duration = Math.max(endMin - startMin, 20);
|
||||
const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT;
|
||||
const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 18);
|
||||
const bgColor = ev.source_color ? `${ev.source_color}20` : "#6366f120";
|
||||
const bgColor = ev.source_color ? `${ev.source_color}28` : "#6366f128";
|
||||
const colLeft = `calc(44px + ${i} * ((100% - 44px) / 7) + 2px)`;
|
||||
const colWidth = `calc((100% - 44px) / 7 - 4px)`;
|
||||
const showMeta = heightPx >= 36;
|
||||
const timeStr = `${this.formatTime(ev.start_time)}\u2013${this.formatTime(ev.end_time)}`;
|
||||
const locName = ev.location_name ? this.esc(ev.location_name) : "";
|
||||
const virtualBadge = ev.is_virtual ? `<span class="wk-virtual" title="Virtual">\u{1F4BB}</span>` : "";
|
||||
eventsOverlay += `<div class="week-event" data-event-id="${ev.id}" style="
|
||||
top:${topPx}px;height:${heightPx}px;left:${colLeft};width:${colWidth};
|
||||
background:${bgColor};border-left-color:${ev.source_color || "#6366f1"}">
|
||||
<div class="week-event-title">${this.esc(ev.title)}</div>
|
||||
<div class="week-event-title">${virtualBadge}${this.esc(ev.title)}</div>
|
||||
${showMeta ? `<div class="week-event-meta"><span class="week-event-time">${timeStr}</span>${locName ? `<span class="week-event-loc">${locName}</span>` : ""}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1229,15 +1339,18 @@ class FolkCalendarView extends HTMLElement {
|
|||
<button class="dd-close" id="dd-close">\u2715</button>
|
||||
</div>
|
||||
${dayEvents.length === 0 ? `<div class="dd-empty">No events</div>` :
|
||||
dayEvents.sort((a, b) => a.start_time.localeCompare(b.start_time)).map(e => `
|
||||
<div class="dd-event" data-event-id="${e.id}">
|
||||
dayEvents.sort((a, b) => a.start_time.localeCompare(b.start_time)).map(e => {
|
||||
const ddDesc = e.description ? (e.description.length > 80 ? e.description.slice(0, 77) + "..." : e.description) : "";
|
||||
const srcTag = e.source_name ? `<span class="dd-source" style="border-color:${e.source_color || '#666'};color:${e.source_color || '#aaa'}">${this.esc(e.source_name)}</span>` : "";
|
||||
return `<div class="dd-event" data-event-id="${e.id}">
|
||||
<div class="dd-color" style="background:${e.source_color || "#6366f1"}"></div>
|
||||
<div class="dd-info">
|
||||
<div class="dd-title">${this.esc(e.title)}</div>
|
||||
<div class="dd-title">${this.esc(e.title)}${srcTag}</div>
|
||||
<div class="dd-meta">${this.formatTime(e.start_time)}${e.end_time ? ` \u2013 ${this.formatTime(e.end_time)}` : ""}${e.location_name ? ` \u00B7 ${this.esc(e.location_name)}` : ""}${e.is_virtual ? " \u00B7 Virtual" : ""}</div>
|
||||
${ddDesc ? `<div class="dd-desc">${this.esc(ddDesc)}</div>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>`;
|
||||
}).join("")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1717,6 +1830,8 @@ class FolkCalendarView extends HTMLElement {
|
|||
.ev-label:hover { background: rgba(255,255,255,0.08); }
|
||||
.ev-time { color: #666; font-size: 8px; margin-right: 2px; }
|
||||
.ev-bell { margin-right: 2px; font-size: 8px; }
|
||||
.ev-loc { color: #64748b; font-size: 7px; margin-left: 3px; }
|
||||
.ev-virtual { font-size: 8px; margin-right: 2px; vertical-align: middle; }
|
||||
|
||||
/* ── Drop Target ── */
|
||||
.day.drop-target { background: rgba(245,158,11,0.15); border: 2px dashed #f59e0b; }
|
||||
|
|
@ -1733,6 +1848,8 @@ class FolkCalendarView extends HTMLElement {
|
|||
.dd-title { font-size: 13px; font-weight: 500; color: #e2e8f0; }
|
||||
.dd-meta { font-size: 11px; color: #94a3b8; margin-top: 2px; }
|
||||
.dd-empty { font-size: 12px; color: #64748b; padding: 8px 0; }
|
||||
.dd-desc { font-size: 11px; color: #64748b; margin-top: 3px; line-height: 1.4; }
|
||||
.dd-source { font-size: 9px; padding: 1px 6px; border-radius: 8px; border: 1px solid; margin-left: 6px; vertical-align: middle; }
|
||||
|
||||
/* ── Event Modal ── */
|
||||
.modal-bg { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
|
|
@ -1755,6 +1872,9 @@ class FolkCalendarView extends HTMLElement {
|
|||
.tl-event-title { font-weight: 600; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tl-event-time { font-size: 10px; color: #94a3b8; }
|
||||
.tl-event-loc { font-size: 10px; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tl-event-desc { font-size: 9px; color: #64748b; margin-top: 2px; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tl-breadcrumb { font-size: 8px; color: #4a5568; background: rgba(255,255,255,0.04); padding: 1px 5px; border-radius: 3px; margin-top: 2px; display: inline-block; }
|
||||
.tl-virtual { font-size: 9px; color: #818cf8; margin-left: 6px; }
|
||||
.now-line { position: absolute; left: 0; right: 0; height: 2px; background: #ef4444; z-index: 5; }
|
||||
.now-dot { position: absolute; left: -5px; top: -3px; width: 8px; height: 8px; border-radius: 50%; background: #ef4444; }
|
||||
|
||||
|
|
@ -1773,11 +1893,18 @@ class FolkCalendarView extends HTMLElement {
|
|||
.week-event { position: absolute; left: 2px; right: 2px; border-radius: 4px; padding: 2px 4px; font-size: 10px; overflow: hidden; cursor: pointer; border-left: 2px solid; z-index: 1; }
|
||||
.week-event:hover { opacity: 0.85; }
|
||||
.week-event-title { font-weight: 600; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.week-event-meta { font-size: 9px; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.week-event-time { color: #64748b; }
|
||||
.week-event-loc { margin-left: 3px; color: #4a5568; }
|
||||
.wk-virtual { font-size: 9px; margin-right: 2px; vertical-align: middle; }
|
||||
|
||||
/* ── Season View ── */
|
||||
.season-header { text-align: center; margin-bottom: 12px; font-size: 16px; font-weight: 600; color: #e2e8f0; }
|
||||
.season-q { font-size: 12px; color: #64748b; font-weight: 400; margin-left: 4px; }
|
||||
.season-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.season-month-wrap { display: flex; flex-direction: column; gap: 4px; }
|
||||
.season-cities { display: flex; flex-wrap: wrap; gap: 3px; padding: 2px 4px; }
|
||||
.season-city-chip { font-size: 9px; color: #94a3b8; background: rgba(255,255,255,0.04); border: 1px solid #333; border-radius: 8px; padding: 1px 6px; white-space: nowrap; }
|
||||
|
||||
/* ── Year View ── */
|
||||
.year-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
||||
|
|
@ -1794,7 +1921,8 @@ class FolkCalendarView extends HTMLElement {
|
|||
.mini-day:hover { background: rgba(255,255,255,0.08); }
|
||||
.mini-day.today { background: #4f46e5; color: #fff; font-weight: 700; }
|
||||
.mini-day.empty { cursor: default; }
|
||||
.mini-dot { position: absolute; bottom: 1px; left: 50%; transform: translateX(-50%); width: 3px; height: 3px; border-radius: 50%; }
|
||||
.mini-dots { position: absolute; bottom: 1px; left: 50%; transform: translateX(-50%); display: flex; gap: 1px; }
|
||||
.mini-dot { width: 3px; height: 3px; border-radius: 50%; flex-shrink: 0; }
|
||||
.mini-hint { text-align: center; font-size: 11px; color: #4a5568; margin-top: 8px; }
|
||||
|
||||
/* ── Transition Animations ── */
|
||||
|
|
@ -1842,6 +1970,8 @@ class FolkCalendarView extends HTMLElement {
|
|||
.dh-event:hover { opacity: 0.85; }
|
||||
.dh-now { position: absolute; top: 0; bottom: 0; width: 2px; background: #ef4444; z-index: 5; }
|
||||
.dh-now::before { content: ""; position: absolute; top: -3px; left: -3px; width: 8px; height: 8px; border-radius: 50%; background: #ef4444; }
|
||||
.dh-event-loc { font-size: 9px; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.dh-virtual { font-size: 9px; margin-right: 2px; vertical-align: middle; }
|
||||
|
||||
/* ── Month Transposed View ── */
|
||||
.month-transposed { overflow-x: auto; }
|
||||
|
|
@ -1855,7 +1985,9 @@ class FolkCalendarView extends HTMLElement {
|
|||
.mt-cell.weekend { background: rgba(255,255,255,0.02); }
|
||||
.mt-cell.empty { background: transparent; border-color: transparent; cursor: default; }
|
||||
.mt-num { font-size: 12px; color: #94a3b8; font-weight: 500; }
|
||||
.mt-count { font-size: 9px; color: #fff; padding: 1px 5px; border-radius: 8px; font-weight: 600; }
|
||||
.mt-color-bar { display: flex; height: 3px; border-radius: 2px; overflow: hidden; width: 100%; position: absolute; bottom: 2px; left: 0; }
|
||||
.mt-seg { height: 100%; min-width: 2px; }
|
||||
.mt-count-num { font-size: 9px; color: #94a3b8; font-weight: 600; }
|
||||
|
||||
/* ── Year Vertical View ── */
|
||||
.year-vertical { max-height: 600px; overflow-y: auto; }
|
||||
|
|
@ -1867,7 +1999,9 @@ class FolkCalendarView extends HTMLElement {
|
|||
.yv-day:hover { background: rgba(255,255,255,0.08); color: #e2e8f0; }
|
||||
.yv-day.today { background: #4f46e5; color: #fff; font-weight: 700; }
|
||||
.yv-day.weekend { opacity: 0.5; }
|
||||
.yv-dot { position: absolute; bottom: 1px; left: 50%; transform: translateX(-50%); width: 3px; height: 3px; border-radius: 50%; }
|
||||
.yv-dots { position: absolute; bottom: 1px; left: 50%; transform: translateX(-50%); display: flex; gap: 1px; }
|
||||
.yv-dot { width: 3px; height: 3px; border-radius: 50%; flex-shrink: 0; }
|
||||
.yv-country { font-size: 9px; color: #4a5568; margin-left: 6px; font-weight: 400; }
|
||||
|
||||
/* ── Multi-Year View ── */
|
||||
.multi-year-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
|
|
@ -1879,7 +2013,8 @@ class FolkCalendarView extends HTMLElement {
|
|||
.micro-month { display: flex; align-items: center; gap: 2px; padding: 2px 3px; border-radius: 3px; cursor: pointer; overflow: hidden; }
|
||||
.micro-month:hover { background: rgba(255,255,255,0.08); }
|
||||
.micro-label { font-size: 8px; color: #4a5568; font-weight: 600; width: 8px; flex-shrink: 0; }
|
||||
.micro-bar { height: 3px; background: #6366f1; border-radius: 2px; flex-shrink: 0; }
|
||||
.micro-bar-stack { height: 3px; border-radius: 2px; flex-shrink: 0; display: flex; overflow: hidden; }
|
||||
.micro-seg { height: 100%; min-width: 1px; }
|
||||
.micro-count { font-size: 7px; color: #64748b; flex-shrink: 0; }
|
||||
|
||||
/* ── Keyboard Hint ── */
|
||||
|
|
@ -1903,6 +2038,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
.sources { gap: 4px; }
|
||||
.src-badge { font-size: 9px; padding: 2px 6px; }
|
||||
.wd { font-size: 10px; padding: 3px; }
|
||||
.season-cities, .week-event-meta, .tl-event-desc, .yv-country { display: none; }
|
||||
.week-view { font-size: 10px; }
|
||||
.zoom-track { min-width: 150px; }
|
||||
.zoom-ctrl { gap: 4px; padding: 6px 8px; }
|
||||
|
|
@ -1924,6 +2060,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
.multi-year-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mini-day { font-size: 8px; }
|
||||
.mini-month-title { font-size: 10px; }
|
||||
.ev-loc, .dd-desc { display: none; }
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue