Update layout

This commit is contained in:
2026-07-17 20:23:52 +02:00
parent 74284ada97
commit b0e873c4c7
8 changed files with 307 additions and 34 deletions
+24 -9
View File
@@ -36,9 +36,11 @@ function addDays(dateStr, n) {
function localParts(date, tz) {
// ponytail: no TZID means "floating" time per RFC 5545 - interpret in the
// system's local zone (matches how node-ical encodes floating DATE values),
// not UTC.
// not UTC. A real tz (incl. "Etc/UTC" for Z-suffixed timestamps) is an
// absolute instant - always display it in Amsterdam, not whatever zone
// the source event happened to carry.
const fmt = new Intl.DateTimeFormat('en-CA', {
...(tz ? { timeZone: tz } : {}),
...(tz ? { timeZone: 'Europe/Amsterdam' } : {}),
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
});
@@ -58,9 +60,9 @@ function formatHour(hour) {
}
function segmentsForDay(dayStr, startLocal, lastDayStr, endLocal, isFullDay) {
if (isFullDay) return [0, 1, 2];
const dayStartHour = dayStr === startLocal.dateStr ? startLocal.hour : 0;
const dayEndHour = dayStr === lastDayStr ? endLocal.hour : 24;
const dayStartHour = isFullDay ? 0 : (dayStr === startLocal.dateStr ? startLocal.hour : 0);
const dayEndHour = isFullDay ? 24 : (dayStr === lastDayStr ? endLocal.hour : 24);
if (isFullDay) return { lit: [0, 1, 2], start: dayStartHour, end: dayEndHour };
const lit = [];
SEGMENT_RANGES.forEach(([segStart, segEnd], i) => {
if (dayStartHour < segEnd && dayEndHour > segStart) lit.push(i);
@@ -69,7 +71,17 @@ function segmentsForDay(dayStr, startLocal, lastDayStr, endLocal, isFullDay) {
const idx = SEGMENT_RANGES.findIndex(([, segEnd]) => dayStartHour < segEnd);
lit.push(idx === -1 ? 2 : idx);
}
return lit;
return { lit, start: dayStartHour, end: dayEndHour };
}
// two tagged events sharing a day overlap if their local-hour spans intersect
function hasOverlap(intervals) {
for (let i = 0; i < intervals.length; i++) {
for (let j = i + 1; j < intervals.length; j++) {
if (intervals[i].start < intervals[j].end && intervals[j].start < intervals[i].end) return true;
}
}
return false;
}
function buildOverview(icsText, tagsConfig, rangeStartStr, rangeEndStr) {
@@ -79,7 +91,7 @@ function buildOverview(icsText, tagsConfig, rangeStartStr, rangeEndStr) {
const days = {};
const getDay = (dateStr) => {
if (!days[dateStr]) days[dateStr] = { segments: [null, null, null], events: [] };
if (!days[dateStr]) days[dateStr] = { segments: [null, null, null], events: [], taggedIntervals: [] };
return days[dateStr];
};
@@ -99,6 +111,8 @@ function buildOverview(icsText, tagsConfig, rangeStartStr, rangeEndStr) {
const summary = inst.summary || '(untitled)';
const colors = leadingEmojis(summary).map((e) => (tagsConfig[e] ? tagsConfig[e].color : UNKNOWN_EMOJI_COLOR));
const color = colors.length === 2 ? colors : colors[0] || null;
// "nc" as a standalone word anywhere in the title marks a conflict as resolved
const noConflict = /\bnc\b/i.test(summary);
const startLocal = localParts(inst.start, inst.start.tz);
const endInstant = isFullDay
@@ -111,11 +125,12 @@ function buildOverview(icsText, tagsConfig, rangeStartStr, rangeEndStr) {
while (true) {
if (dayStr >= rangeStartStr && dayStr <= rangeEndStr) {
const day = getDay(dayStr);
const lit = segmentsForDay(dayStr, startLocal, lastDayStr, endLocal, isFullDay);
const { lit, start, end } = segmentsForDay(dayStr, startLocal, lastDayStr, endLocal, isFullDay);
for (const segIdx of lit) {
if (color) day.segments[segIdx] = color;
else if (!day.segments[segIdx]) day.segments[segIdx] = 'muted';
}
if (color && !noConflict) day.taggedIntervals.push({ start, end });
day.events.push({
hour: dayStr === startLocal.dateStr ? startLocal.hour : 0,
isFullDay,
@@ -133,7 +148,7 @@ function buildOverview(icsText, tagsConfig, rangeStartStr, rangeEndStr) {
const lines = day.events
.sort((a, b) => a.hour - b.hour)
.map((e) => `${e.isFullDay ? 'All day' : formatHour(e.hour)} ${e.text}`);
result[dateStr] = { segments: day.segments, tooltip: lines.join('\n') };
result[dateStr] = { segments: day.segments, tooltip: lines.join('\n'), conflict: hasOverlap(day.taggedIntervals) };
}
return result;
}