This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
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)