import calendarUtil from "../calendarUtil.js"; class DesktopSidebar extends Shadow { static DAY_NAMES = ["S", "M", "T", "W", "T", "F", "S"]; constructor(currentDate, calendars, selectedCalendars, events, weekStartsOn, actions) { super() this.currentDate = currentDate this.calendars = calendars this.selectedCalendars = selectedCalendars this.events = events this.weekStartsOn = weekStartsOn this.actions = actions // Persist mini calendar month across parent rerenders if (this.miniDate === undefined) { this.miniDate = new Date(currentDate) } if (this.selectedDate === undefined) { this.selectedDate = new Date(currentDate) } } render() { const { onSelectDate, onToggleCalendar, onNewCalendar, onEditCalendar } = this.actions const ordered = Array.from({ length: 7 }, (_, i) => DesktopSidebar.DAY_NAMES[(this.weekStartsOn + i) % 7] ); const weeks = this.buildMonthWeeks(this.miniDate); const today = new Date(); VStack(() => { // ── Mini calendar ───────────────────────────────────────── VStack(() => { // Month / year + nav arrows HStack(() => { p(this.miniDate.toLocaleDateString(undefined, { month: "long", year: "numeric" })) .margin(0) .fontWeight("600") .fontSize(0.78, em) .color("var(--headertext)") .flex(1) HStack(() => { button("‹") .onClick((done) => {if(!done) return; this.miniDate = calendarUtil.addMonths(this.miniDate, -1); this.rerender(); }) .padding("0.18em 0.42em") .border("none") .background("transparent") .color("var(--headertext)") .cursor("pointer") .borderRadius(0.3, em) .fontSize(0.85, em) button("›") .onClick((done) => {if(!done) return; this.miniDate = calendarUtil.addMonths(this.miniDate, 1); this.rerender(); }) .padding("0.18em 0.42em") .border("none") .background("transparent") .color("var(--headertext)") .cursor("pointer") .borderRadius(0.3, em) .fontSize(0.85, em) }) .gap(0) }) .alignItems("center") .marginBottom(0.35, em) // Day-name header row HStack(() => { ordered.forEach(name => { p(name) .margin(0) .fontSize(0.64, em) .fontWeight("600") .color("var(--headertext)") .opacity(0.42) .flex(1) .textAlign("center") .paddingVertical(0.18, em) }) }) .width(100, pct) // Date cells VStack(() => { weeks.forEach(({ days }) => { HStack(() => { days.forEach(({ day, isCurrentMonth, calendarColors }) => { const isToday = calendarUtil.isSameDay(day, today); const isSelected = calendarUtil.isSameDay(day, this.selectedDate); VStack(() => { p(day.getDate()) .margin(0) .fontSize(0.72, em) .fontWeight(isSelected ? "700" : "400") .color(isSelected || isToday ? "white" : "var(--headertext)") .background( isToday ? "var(--quillred)" : isSelected ? "var(--lightaccent)" : "transparent" ) .width(1.52, em) .height(1.52, em) .borderRadius(25, pct) .textAlign("center") .lineHeight("1.52em") .opacity(isCurrentMonth ? 1 : 0.27) .boxSizing("border-box") if (calendarColors.length > 0) { HStack(() => { calendarColors.slice(0, 3).forEach(color => { VStack(() => {}) .width(0.24, em) .height(0.24, em) .borderRadius(50, pct) .background(color) .flexShrink(0) }) }) .gap(0.14, em) .justifyContent("center") .marginTop(0.1, em) } else { VStack(() => {}).height(0.34, em) } }) .flex(1) .alignItems("center") .paddingVertical(0.16, em) .cursor("pointer") .onClick((done) => {if(!done) return; this.selectedDate = day; this.miniDate = new Date(day); onSelectDate(day); this.rerender(); }) }) }) .width(100, pct) }) }) .width(100, pct) }) .marginTop(30, px) .padding(1, em) .paddingBottom(0.75, em) // ── Divider ─────────────────────────────────────────────── VStack(() => {}) .height(1, px) .background("var(--divider)") .width(100, pct) // ── Calendars list ──────────────────────────────────────── VStack(() => { p("CALENDARS") .margin(0) .marginBottom(0.45, em) .fontSize(0.63, em) .fontWeight("700") .letterSpacing("0.07em") .color("var(--headertext)") .opacity(0.38) .paddingHorizontal(1, em) this.calendars.forEach(cal => { const isSelected = this.selectedCalendars.some(c => c.id === cal.id); HStack(() => { HStack(() => { VStack(() => {}) .width(0.65, em) .height(0.65, em) .borderRadius(50, pct) .background(isSelected ? cal.color : "transparent") .border(`2px solid ${cal.color}`) .boxSizing("border-box") .flexShrink(0) p(cal.name) .margin(0) .fontSize(0.78, em) .color("var(--headertext)") .opacity(isSelected ? 1 : 0.4) .flex(1) .overflow("hidden") .whiteSpace("nowrap") .textOverflow("ellipsis") }) .flex(1) .gap(0.55, em) .alignItems("center") .cursor("pointer") .onClick((done) => { if (!done) return; if (onToggleCalendar) onToggleCalendar(cal); }) if (onEditCalendar && cal.owner_id === global.profile.id) { button("···") .border("none") .background("transparent") .color("var(--headertext)") .opacity(0.4) .fontSize(0.72, em) .cursor("pointer") .padding("0 0.2em") .flexShrink(0) .onClick((done) => { if (done) onEditCalendar(cal) }) } }) .paddingHorizontal(1, em) .paddingVertical(0.32, em) .alignItems("center") .borderRadius(0.4, em) }) button("+ New Calendar") .paddingVertical(0.52, em) .paddingHorizontal(0.8, em) .marginHorizontal(1, em) .marginVertical(0.5, em) .width("auto") .background("transparent") .border("1px solid var(--divider)") .color("var(--headertext)") .borderRadius(0.5, em) .fontSize(0.83, em) .fontWeight("600") .cursor("pointer") .onHover(function(hovering) { this.style.background = hovering ? "var(--divider)" : "transparent"; }) .onClick((done) => { if (done) onNewCalendar() }) }) .paddingTop(0.7, em) .paddingBottom(0.5, em) }) .width(220, px) .minWidth(220, px) .height(100, pct) .borderRight("1px solid var(--divider)") .boxSizing("border-box") .overflowY("auto") .flexShrink(0) } buildMonthWeeks(date) { const month = date.getMonth(); const allDays = calendarUtil.buildMonthGrid(date, this.weekStartsOn); const gridStart = allDays[0]; const gridEnd = calendarUtil.endOfDay(allDays[41]); const colorsByDay = new Map(); allDays.forEach(day => colorsByDay.set(calendarUtil.toDateInput(day), [])); const colorByCalId = new Map((this.selectedCalendars || []).map(c => [c.id, c.color])); const expanded = calendarUtil.expandRecurringEvents(this.events || [], gridStart, gridEnd); expanded.filter(ev => { const end = ev.all_day ? calendarUtil.endOfDay(ev.time_end) : ev.time_end; return calendarUtil.rangesOverlap(ev.time_start, end, gridStart, gridEnd); }).forEach(ev => { const colors = (ev.calendars || []) .map(id => colorByCalId.get(id)) .filter(Boolean); if (colors.length === 0) return; const effectiveEnd = ev.all_day ? calendarUtil.endOfDay(ev.time_end) : ev.time_end; let cursor = calendarUtil.startOfDay(ev.time_start > gridStart ? ev.time_start : gridStart); const end = effectiveEnd < gridEnd ? effectiveEnd : gridEnd; while (cursor < end) { const key = calendarUtil.toDateInput(cursor); const arr = colorsByDay.get(key); if (arr) { colors.forEach(color => { if (!arr.includes(color) && arr.length < 3) arr.push(color); }); } cursor = calendarUtil.addDays(cursor, 1); } }); const weeks = []; for (let w = 0; w < 6; w++) { weeks.push({ days: allDays.slice(w * 7, w * 7 + 7).map(day => ({ day, isCurrentMonth: day.getMonth() === month, calendarColors: colorsByDay.get(calendarUtil.toDateInput(day)) || [] })) }); } return weeks; } } register(DesktopSidebar)