Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m22s
Details
CI/CD / deploy (push) Successful in 2m22s
Details
This commit is contained in:
commit
e32e80fc09
|
|
@ -1149,6 +1149,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
const todayStr = this.dateStr(today);
|
||||
|
||||
let html = "";
|
||||
let detailRenderedInline = false;
|
||||
const prevDays = new Date(year, month, 0).getDate();
|
||||
for (let i = firstDay - 1; i >= 0; i--) {
|
||||
html += `<div class="day other"><div class="day-num">${prevDays - i}</div></div>`;
|
||||
|
|
@ -1199,6 +1200,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
const posInRow = cellIndex % 7;
|
||||
if (posInRow === 6 || d === daysInMonth) {
|
||||
html += this.renderDayDetail(ds, dayEvents);
|
||||
detailRenderedInline = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1209,7 +1211,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
html += `<div class="day other"><div class="day-num">${i}</div></div>`;
|
||||
}
|
||||
|
||||
if (this.expandedDay) {
|
||||
if (this.expandedDay && !detailRenderedInline) {
|
||||
const dayEvents = this.getEventsForDate(this.expandedDay);
|
||||
html += this.renderDayDetail(this.expandedDay, dayEvents);
|
||||
}
|
||||
|
|
@ -1383,19 +1385,30 @@ class FolkCalendarView extends HTMLElement {
|
|||
hoursHtml += `<div class="dh-hour" style="left:${(h - START_HOUR) * HOUR_WIDTH}px;width:${HOUR_WIDTH}px">${label}</div>`;
|
||||
}
|
||||
|
||||
// Compute row assignments for overlapping horizontal events
|
||||
const dhIntervals = timed.map(ev => {
|
||||
const s = new Date(ev.start_time), e = new Date(ev.end_time);
|
||||
const startMin = s.getHours() * 60 + s.getMinutes();
|
||||
const endMin = e.getHours() * 60 + e.getMinutes();
|
||||
return { startMin, endMin: Math.max(endMin, startMin + 30) };
|
||||
});
|
||||
const dhLayout = this.computeEventColumns(dhIntervals);
|
||||
const DH_ROW_HEIGHT = 56; // min-height 48 + 8px gap
|
||||
|
||||
let eventsHtml = "";
|
||||
for (const ev of timed) {
|
||||
const start = new Date(ev.start_time), end = new Date(ev.end_time);
|
||||
const startMin = start.getHours() * 60 + start.getMinutes();
|
||||
const endMin = end.getHours() * 60 + end.getMinutes();
|
||||
const duration = Math.max(endMin - startMin, 30);
|
||||
for (let idx = 0; idx < timed.length; idx++) {
|
||||
const ev = timed[idx];
|
||||
const { startMin, endMin } = dhIntervals[idx];
|
||||
const { col: row } = dhLayout[idx];
|
||||
const duration = endMin - startMin;
|
||||
const leftPx = ((startMin - START_HOUR * 60) / 60) * HOUR_WIDTH;
|
||||
const widthPx = Math.max((duration / 60) * HOUR_WIDTH, 60);
|
||||
const topPx = 8 + row * DH_ROW_HEIGHT;
|
||||
const es = this.getEventStyles(ev);
|
||||
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:${es.bgColor};border-top:3px ${es.borderStyle} ${ev.source_color || "#6366f1"};opacity:${es.opacity}">
|
||||
left:${leftPx}px;width:${widthPx}px;top:${topPx}px;background:${es.bgColor};border-top:3px ${es.borderStyle} ${ev.source_color || "#6366f1"};opacity:${es.opacity}">
|
||||
<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>` : ""}
|
||||
|
|
@ -1640,6 +1653,67 @@ class FolkCalendarView extends HTMLElement {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// ── Event Layout Helper (overlap columns) ──
|
||||
|
||||
/**
|
||||
* Assign columns to overlapping events so they tile side-by-side.
|
||||
* Returns an array parallel to `events` with {col, totalCols} per event.
|
||||
*/
|
||||
private computeEventColumns(events: Array<{startMin: number, endMin: number}>): Array<{col: number, totalCols: number}> {
|
||||
const n = events.length;
|
||||
if (n === 0) return [];
|
||||
|
||||
// Sort indices by start, then by longest duration first
|
||||
const indices = Array.from({length: n}, (_, i) => i);
|
||||
indices.sort((a, b) => events[a].startMin - events[b].startMin
|
||||
|| (events[b].endMin - events[b].startMin) - (events[a].endMin - events[a].startMin));
|
||||
|
||||
const cols = new Array<number>(n).fill(0);
|
||||
const colEnds: number[] = []; // end-time of last event in each column
|
||||
|
||||
// Track overlap clusters to set per-cluster totalCols
|
||||
const clusterIndices: number[][] = [[]]; // indices in current cluster
|
||||
let clusterMaxEnd = -Infinity;
|
||||
|
||||
for (const i of indices) {
|
||||
const ev = events[i];
|
||||
|
||||
// New cluster? (no overlap with any previous event)
|
||||
if (colEnds.length > 0 && ev.startMin >= clusterMaxEnd) {
|
||||
clusterIndices.push([]);
|
||||
colEnds.length = 0;
|
||||
}
|
||||
|
||||
// Find first free column
|
||||
let placed = -1;
|
||||
for (let c = 0; c < colEnds.length; c++) {
|
||||
if (colEnds[c] <= ev.startMin) {
|
||||
colEnds[c] = ev.endMin;
|
||||
placed = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (placed === -1) {
|
||||
placed = colEnds.length;
|
||||
colEnds.push(ev.endMin);
|
||||
}
|
||||
|
||||
cols[i] = placed;
|
||||
clusterMaxEnd = Math.max(clusterMaxEnd, ev.endMin);
|
||||
clusterIndices[clusterIndices.length - 1].push(i);
|
||||
}
|
||||
|
||||
// Assign totalCols per cluster
|
||||
const result = new Array<{col: number, totalCols: number}>(n);
|
||||
for (const cluster of clusterIndices) {
|
||||
const maxCol = cluster.reduce((m, i) => Math.max(m, cols[i]), 0) + 1;
|
||||
for (const i of cluster) {
|
||||
result[i] = { col: cols[i], totalCols: maxCol };
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Day View ──
|
||||
|
||||
private renderDay(): string {
|
||||
|
|
@ -1668,12 +1742,21 @@ class FolkCalendarView extends HTMLElement {
|
|||
hoursHtml += `<div class="hour-row"><span class="hour-label">${label}</span><div class="hour-content"></div></div>`;
|
||||
}
|
||||
|
||||
// Compute overlap columns for timed events
|
||||
const timedIntervals = timed.map(ev => {
|
||||
const s = new Date(ev.start_time), e = new Date(ev.end_time);
|
||||
const startMin = s.getHours() * 60 + s.getMinutes();
|
||||
const endMin = e.getHours() * 60 + e.getMinutes();
|
||||
return { startMin, endMin: Math.max(endMin, startMin + 30) };
|
||||
});
|
||||
const layout = this.computeEventColumns(timedIntervals);
|
||||
|
||||
let eventsHtml = "";
|
||||
for (const ev of timed) {
|
||||
const start = new Date(ev.start_time), end = new Date(ev.end_time);
|
||||
const startMin = start.getHours() * 60 + start.getMinutes();
|
||||
const endMin = end.getHours() * 60 + end.getMinutes();
|
||||
const duration = Math.max(endMin - startMin, 30);
|
||||
for (let idx = 0; idx < timed.length; idx++) {
|
||||
const ev = timed[idx];
|
||||
const { startMin, endMin } = timedIntervals[idx];
|
||||
const { col, totalCols } = layout[idx];
|
||||
const duration = endMin - startMin;
|
||||
const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT;
|
||||
const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 24);
|
||||
const es = this.getEventStyles(ev);
|
||||
|
|
@ -1684,7 +1767,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
const likelihoodBadge = es.isTentative ? `<span class="ev-likelihood">${es.likelihoodLabel}</span>` : "";
|
||||
|
||||
eventsHtml += `<div class="tl-event" data-event-id="${ev.id}" style="
|
||||
top:${topPx}px;height:${heightPx}px;background:${es.bgColor};border-left-color:${ev.source_color || "#6366f1"};border-left-style:${es.borderStyle};opacity:${es.opacity}">
|
||||
top:${topPx}px;height:${heightPx}px;left:calc(8px + ${col} * (100% - 16px) / ${totalCols});width:calc((100% - 16px) / ${totalCols} - 2px);background:${es.bgColor};border-left-color:${ev.source_color || "#6366f1"};border-left-style:${es.borderStyle};opacity:${es.opacity}">
|
||||
<div class="tl-event-title">${this.esc(ev.title)}${likelihoodBadge}</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>` : ""}
|
||||
|
|
@ -1758,17 +1841,30 @@ class FolkCalendarView extends HTMLElement {
|
|||
let eventsOverlay = "";
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const ds = this.dateStr(days[i]);
|
||||
for (const ev of this.getEventsForDate(ds)) {
|
||||
const start = new Date(ev.start_time), end = new Date(ev.end_time);
|
||||
if ((end.getTime() - start.getTime()) >= 86400000) continue;
|
||||
const startMin = start.getHours() * 60 + start.getMinutes();
|
||||
const endMin = end.getHours() * 60 + end.getMinutes();
|
||||
const duration = Math.max(endMin - startMin, 20);
|
||||
const dayTimed = this.getEventsForDate(ds).filter(ev => {
|
||||
const s = new Date(ev.start_time), en = new Date(ev.end_time);
|
||||
return (en.getTime() - s.getTime()) < 86400000;
|
||||
}).sort((a, b) => a.start_time.localeCompare(b.start_time));
|
||||
|
||||
// Compute sub-columns for overlapping events within this day
|
||||
const intervals = dayTimed.map(ev => {
|
||||
const s = new Date(ev.start_time), e = new Date(ev.end_time);
|
||||
const startMin = s.getHours() * 60 + s.getMinutes();
|
||||
const endMin = e.getHours() * 60 + e.getMinutes();
|
||||
return { startMin, endMin: Math.max(endMin, startMin + 20) };
|
||||
});
|
||||
const dayLayout = this.computeEventColumns(intervals);
|
||||
|
||||
for (let j = 0; j < dayTimed.length; j++) {
|
||||
const ev = dayTimed[j];
|
||||
const { startMin, endMin } = intervals[j];
|
||||
const { col, totalCols } = dayLayout[j];
|
||||
const duration = endMin - startMin;
|
||||
const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT;
|
||||
const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 18);
|
||||
const es = this.getEventStyles(ev);
|
||||
const colLeft = `calc(44px + ${i} * ((100% - 44px) / 7) + 2px)`;
|
||||
const colWidth = `calc((100% - 44px) / 7 - 4px)`;
|
||||
const colLeft = `calc(44px + ${i} * ((100% - 44px) / 7) + 2px + ${col} * ((100% - 44px) / 7 - 4px) / ${totalCols})`;
|
||||
const colWidth = `calc(((100% - 44px) / 7 - 4px) / ${totalCols} - 1px)`;
|
||||
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) : "";
|
||||
|
|
@ -2785,10 +2881,10 @@ class FolkCalendarView extends HTMLElement {
|
|||
.synodic-marker { position: absolute; top: -2px; font-size: 12px; transform: translateX(-50%); pointer-events: none; }
|
||||
|
||||
/* ── Month Grid ── */
|
||||
.weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 4px; }
|
||||
.weekdays { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; margin-bottom: 4px; }
|
||||
.wd { text-align: center; font-size: 11px; color: var(--rs-text-muted); padding: 4px; font-weight: 600; }
|
||||
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; position: relative; flex: 1; }
|
||||
.day { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 6px; min-height: 0; padding: 6px; cursor: pointer; position: relative; -webkit-tap-highlight-color: transparent; }
|
||||
.grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; position: relative; flex: 1; }
|
||||
.day { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 6px; min-height: 0; min-width: 0; overflow: hidden; padding: 6px; cursor: pointer; position: relative; -webkit-tap-highlight-color: transparent; }
|
||||
.day:hover { border-color: var(--rs-border-strong); }
|
||||
.day.today { border-color: var(--rs-primary-hover); background: var(--rs-bg-active); }
|
||||
.day.expanded { border-color: var(--rs-primary-hover); background: rgba(99,102,241,0.1); }
|
||||
|
|
@ -2916,7 +3012,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
.hour-row { display: flex; min-height: 48px; border-bottom: 1px solid var(--rs-border-subtle); position: relative; }
|
||||
.hour-label { position: absolute; left: -48px; top: -7px; width: 40px; text-align: right; font-size: 10px; color: var(--rs-text-muted); font-variant-numeric: tabular-nums; }
|
||||
.hour-content { flex: 1; position: relative; padding-left: 8px; }
|
||||
.tl-event { position: absolute; left: 8px; right: 8px; border-radius: 6px; padding: 4px 8px; font-size: 11px; overflow: hidden; cursor: pointer; border-left: 3px solid; z-index: 1; transition: opacity 0.15s; }
|
||||
.tl-event { position: absolute; border-radius: 6px; padding: 4px 8px; font-size: 11px; overflow: hidden; cursor: pointer; border-left: 3px solid; z-index: 1; transition: opacity 0.15s; box-sizing: border-box; }
|
||||
.tl-event:hover { opacity: 0.85; }
|
||||
.tl-event-title { font-weight: 600; color: var(--rs-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tl-event-time { font-size: 10px; color: var(--rs-text-secondary); }
|
||||
|
|
@ -2939,7 +3035,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
.week-time-label { font-size: 10px; color: var(--rs-text-muted); text-align: right; padding-right: 6px; font-variant-numeric: tabular-nums; height: 48px; }
|
||||
.week-cell { border-left: 1px solid var(--rs-border-subtle); border-bottom: 1px solid var(--rs-border-subtle); min-height: 48px; position: relative; }
|
||||
.week-cell.today { background: rgba(99,102,241,0.04); }
|
||||
.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 { position: absolute; border-radius: 4px; padding: 2px 4px; font-size: 10px; overflow: hidden; cursor: pointer; border-left: 2px solid; z-index: 1; box-sizing: border-box; }
|
||||
.week-event:hover { opacity: 0.85; }
|
||||
.week-event-title { font-weight: 600; color: var(--rs-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.week-event-meta { font-size: 9px; color: var(--rs-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
|
@ -3015,7 +3111,7 @@ class FolkCalendarView extends HTMLElement {
|
|||
.dh-hours { position: relative; height: 24px; border-bottom: 1px solid var(--rs-border); }
|
||||
.dh-hour { position: absolute; top: 0; height: 24px; font-size: 10px; color: var(--rs-text-muted); text-align: center; border-left: 1px solid var(--rs-border-subtle); line-height: 24px; }
|
||||
.dh-events { position: relative; min-height: 140px; padding-top: 8px; }
|
||||
.dh-event { position: absolute; top: 32px; height: auto; min-height: 48px; border-radius: 6px; padding: 4px 8px; font-size: 11px; overflow: hidden; cursor: pointer; border-top: 3px solid; z-index: 1; }
|
||||
.dh-event { position: absolute; height: auto; min-height: 48px; border-radius: 6px; padding: 4px 8px; font-size: 11px; overflow: hidden; cursor: pointer; border-top: 3px solid; z-index: 1; box-sizing: border-box; }
|
||||
.dh-event:hover { opacity: 0.85; }
|
||||
.dh-now { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--rs-error); z-index: 5; }
|
||||
.dh-now::before { content: ""; position: absolute; top: -3px; left: -3px; width: 8px; height: 8px; border-radius: 50%; background: var(--rs-error); }
|
||||
|
|
|
|||
Loading…
Reference in New Issue