import calendarUtil from "../calendarUtil.js"; css(` monthgrid- { scrollbar-width: none; -ms-overflow-style: none; } monthgrid-::-webkit-scrollbar { display: none; width: 0; height: 0; } `) class MonthGrid extends Shadow { constructor(weeks, calendars, onDayTap = null) { super() this.weeks = weeks; this.calendars = calendars; this.onDayTap = onDayTap; this.maxVisible = 3; // caps both rendering and row height calculation // em this.dateFontSize = 1.2; this.dateLineHeight = 1; this.paddingTop = 1.5; this.paddingBottom = 0.55; // em this.pillHeight = 1.15; this.pillGap = 0.2; this.rowBottomPadding = 0.2; this.headerHeight = this.paddingTop + (this.dateFontSize * this.dateLineHeight) + this.paddingBottom; this.rowHeight = this.headerHeight + this.maxVisible * (this.pillHeight + this.pillGap) + this.rowBottomPadding; } render() { VStack(() => { this.weeks.forEach((week, wi) => { this.renderWeekRow(week, wi === this.weeks.length - 1); }) }) .width(100, pct) .height(this.rowHeight * this.weeks.length, em) .flexShrink(0) .flex(1) .overflowY("scroll") } renderWeekRow(week, isLastWeek) { ZStack(() => { this.renderCellLayer(week, isLastWeek) this.renderPillLayer(week) }) .position("relative") .width(100, pct) .height(this.rowHeight + 0.5, em) .flexShrink(0) .alignItems("stretch") } renderCellLayer(week, isLastWeek) { HStack(() => { week.days.forEach((dayData, di) => { this.renderDayCell(dayData, di === 6, isLastWeek) }) }) .width(100, pct) .alignItems("stretch") .height(100, pct) } renderDayCell(dayData, isLast, isLastWeek) { const { day, isCurrentMonth } = dayData; const today = calendarUtil.isToday(day); VStack(() => { HStack(() => { p(day.getDate()) .margin(0) .fontSize(this.dateFontSize, em) .fontWeight(today ? "700" : "500") .color(today ? "white" : "var(--headertext)") .background(today ? "var(--quillred)" : "transparent") .paddingHorizontal(0.2, em) .paddingVertical(0.125, em) .borderRadius(25, pct) .textAlign("center") .opacity(isCurrentMonth ? 1 : 0) .lineHeight(`${this.dateLineHeight}`) }) .position("relative") .justifyContent("center") .paddingTop(this.paddingTop, em) .paddingHorizontal(0.4, em) .paddingBottom(this.paddingBottom, em) }) .flex(1) .width(0, px) .minWidth(0) .height(100, pct) .borderBottom(isLastWeek ? "1px solid transparent" : "0.5px solid var(--divider)") .boxSizing("border-box") .overflow("hidden") .alignItems("stretch") .cursor("pointer") .onTap(() => { this.onDayTap(day) }) } renderPillLayer(week) { ZStack(() => { const maxSlots = Math.max(0, ...week.slotMap.map(s => s.length)); for (let row = 0; row < Math.min(maxSlots, this.maxVisible); row++) { this.collectSpans(week, row).forEach(span => this.renderPill(span, week, row)) } // Overflow labels week.days.forEach((dayData, col) => { if (!dayData.isCurrentMonth) return; const overflow = Math.max(0, dayData.events.length - this.maxVisible); if (overflow === 0) return; const leftPct = (col / 7) * 100; p(`+${overflow} more`) .margin(0) .fontSize(0.62, em) .fontWeight("600") .color("var(--headertext)") .opacity(0.5) .position("absolute") .bottom(this.rowBottomPadding, em) .left(leftPct, pct) .width(100 / 7, pct) .paddingHorizontal(0.4, em) .zIndex(2) }) }) .position("absolute") .top(0).left(0).right(0).bottom(0) .pointerEvents("none") } renderPill({ startCol, endCol, event }, week, row) { const color = calendarUtil.getCalendarColor(this.calendars, event.calendars.find(id => this.calendars.some(c => c.id === id)) ?? event.calendars[0]); const leftPct = (startCol / 7) * 100; const widthPct = ((endCol - startCol + 1) / 7) * 100; const topEm = this.headerHeight + row * (this.pillHeight + this.pillGap); const clippedLeft = startCol === 0 && calendarUtil.startOfDay(event.time_start) < calendarUtil.startOfDay(week.days[0].day); const clippedRight = endCol === 6 && calendarUtil.endOfDay(event.time_end) > calendarUtil.endOfDay(week.days[6].day); const colWidthPct = 100 / 7; const leftInsetPct = clippedLeft ? 0 : 0.025 * colWidthPct; const rightInsetPct = clippedRight ? 0 : 0.025 * colWidthPct; const brLeft = clippedLeft ? 0 : 5; const brRight = clippedRight ? 0 : 5; HStack(() => { p(event.title || "Untitled") .margin(0) .fontSize(0.9, em) .fontWeight("600") .color("white") .whiteSpace("nowrap") .overflow("hidden") }) .position("absolute") .top(topEm, em) .left(leftPct + leftInsetPct, pct) .width(widthPct - leftInsetPct - rightInsetPct, pct) .height(this.pillHeight, em) .paddingHorizontal(0.4, em) .background(color) .borderTopLeftRadius(`${brLeft}px`) .borderBottomLeftRadius(`${brLeft}px`) .borderTopRightRadius(`${brRight}px`) .borderBottomRightRadius(`${brRight}px`) .alignItems("center") .overflow("hidden") .boxSizing("border-box") .zIndex(1) .pointerEvents("auto") .cursor("pointer") .onTap(() => $("bottomsheet-").showEvent(event)) } collectSpans(week, row) { const spans = []; let current = null; for (let col = 0; col < 7; col++) { const slot = (week.slotMap[col] || [])[row] ?? null; if (slot && current && slot.event === current.event) { current.endCol = col; } else { if (current) spans.push(current); current = slot ? { startCol: col, endCol: col, event: slot.event } : null; } } if (current) spans.push(current); // Only render spans that touch at least one current-month day return spans.filter(span => { for (let col = span.startCol; col <= span.endCol; col++) { if (week.days[col]?.isCurrentMonth) return true; } return false; }); } } register(MonthGrid)