trud-calendar-core 0.1.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -7,6 +7,8 @@ var DEFAULT_LABELS = {
7
7
  week: "Week",
8
8
  day: "Day",
9
9
  agenda: "Agenda",
10
+ year: "Year",
11
+ timeline: "Timeline",
10
12
  allDay: "all-day",
11
13
  noEvents: "No events in this period",
12
14
  more: (n) => `+${n} more`
@@ -19,9 +21,10 @@ var DEFAULT_VIEW = "month";
19
21
  var HOURS_IN_DAY = 24;
20
22
  var MINUTES_IN_HOUR = 60;
21
23
  var MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
22
- var VIEWS = ["month", "week", "day", "agenda"];
24
+ var VIEWS = ["month", "week", "day", "agenda", "year", "timeline"];
23
25
  var DEFAULT_DAY_START_HOUR = 0;
24
26
  var DEFAULT_DAY_END_HOUR = 24;
27
+ var DEFAULT_SNAP_DURATION = 15;
25
28
 
26
29
  // src/utils/date.ts
27
30
  function parseDate(iso) {
@@ -134,8 +137,29 @@ function getVisibleRange(date, view, weekStartsOn = 0) {
134
137
  return { start: date, end: date };
135
138
  case "agenda":
136
139
  return { start: date, end: addDays(date, 30) };
140
+ case "year": {
141
+ const d = parseDate(date);
142
+ const yearStart = `${d.getFullYear()}-01-01`;
143
+ const yearEnd = `${d.getFullYear()}-12-31`;
144
+ return { start: yearStart, end: yearEnd };
145
+ }
146
+ case "timeline":
147
+ return { start: date, end: date };
137
148
  }
138
149
  }
150
+ function getISOWeekNumber(date) {
151
+ const d = parseDate(date);
152
+ const target = new Date(d.getFullYear(), d.getMonth(), d.getDate());
153
+ const dayNum = target.getDay() || 7;
154
+ target.setDate(target.getDate() + 4 - dayNum);
155
+ const yearStart = new Date(target.getFullYear(), 0, 1);
156
+ return Math.ceil(((target.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
157
+ }
158
+ function filterHiddenDays(days, hiddenDays) {
159
+ if (hiddenDays.length === 0) return days;
160
+ const hiddenSet = new Set(hiddenDays);
161
+ return days.filter((d) => !hiddenSet.has(parseDate(d).getDay()));
162
+ }
139
163
  function getTimeOfDay(datetime) {
140
164
  const d = parseDate(datetime);
141
165
  return d.getHours() + d.getMinutes() / 60;
@@ -192,6 +216,15 @@ function formatToolbarTitle(date, view, locale = "en-US") {
192
216
  }).format(d);
193
217
  case "agenda":
194
218
  return getFormatter(locale, { month: "long", year: "numeric" }).format(d);
219
+ case "year":
220
+ return getFormatter(locale, { year: "numeric" }).format(d);
221
+ case "timeline":
222
+ return getFormatter(locale, {
223
+ weekday: "long",
224
+ month: "long",
225
+ day: "numeric",
226
+ year: "numeric"
227
+ }).format(d);
195
228
  }
196
229
  }
197
230
  function formatWeekdayShort(date, locale = "en-US") {
@@ -244,6 +277,10 @@ function navigateByView(date, view, direction) {
244
277
  return addDays(date, direction);
245
278
  case "agenda":
246
279
  return addMonths(date, direction);
280
+ case "year":
281
+ return addMonths(date, 12 * direction);
282
+ case "timeline":
283
+ return addDays(date, direction);
247
284
  }
248
285
  }
249
286
  function calendarReducer(state, action) {
@@ -276,6 +313,261 @@ function calendarReducer(state, action) {
276
313
  }
277
314
  }
278
315
 
316
+ // src/utils/timezone.ts
317
+ var WALL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?$/;
318
+ var UTC_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z$/;
319
+ function pad2(n) {
320
+ return n < 10 ? `0${n}` : `${n}`;
321
+ }
322
+ function parseWall(wall) {
323
+ const m = WALL_RE.exec(wall);
324
+ if (!m) throw new RangeError(`Invalid wall-clock DateTimeString: ${wall}`);
325
+ return {
326
+ year: +m[1],
327
+ month: +m[2],
328
+ day: +m[3],
329
+ hour: +m[4],
330
+ minute: +m[5],
331
+ second: +m[6]
332
+ };
333
+ }
334
+ function formatWall(p) {
335
+ return `${p.year}-${pad2(p.month)}-${pad2(p.day)}T${pad2(p.hour)}:${pad2(p.minute)}:${pad2(p.second)}`;
336
+ }
337
+ function parseUtcMs(utc) {
338
+ if (UTC_RE.test(utc)) return Date.parse(utc);
339
+ if (WALL_RE.test(utc)) return Date.parse(`${utc}Z`);
340
+ throw new RangeError(`Invalid datetime string: ${utc}`);
341
+ }
342
+ function utcMsToIso(ms) {
343
+ return new Date(ms).toISOString().replace(/\.\d+Z$/, "Z");
344
+ }
345
+ var dtfCache = /* @__PURE__ */ new Map();
346
+ function dtfFor(timeZone) {
347
+ let dtf = dtfCache.get(timeZone);
348
+ if (!dtf) {
349
+ dtf = new Intl.DateTimeFormat("en-US", {
350
+ timeZone,
351
+ year: "numeric",
352
+ month: "2-digit",
353
+ day: "2-digit",
354
+ hour: "2-digit",
355
+ minute: "2-digit",
356
+ second: "2-digit",
357
+ hour12: false,
358
+ hourCycle: "h23"
359
+ });
360
+ dtfCache.set(timeZone, dtf);
361
+ }
362
+ return dtf;
363
+ }
364
+ function partsAt(timeZone, utcMs) {
365
+ const parts = dtfFor(timeZone).formatToParts(new Date(utcMs));
366
+ const result = {};
367
+ for (const part of parts) {
368
+ switch (part.type) {
369
+ case "year":
370
+ result.year = +part.value;
371
+ break;
372
+ case "month":
373
+ result.month = +part.value;
374
+ break;
375
+ case "day":
376
+ result.day = +part.value;
377
+ break;
378
+ case "hour":
379
+ result.hour = +part.value % 24;
380
+ break;
381
+ case "minute":
382
+ result.minute = +part.value;
383
+ break;
384
+ case "second":
385
+ result.second = +part.value;
386
+ break;
387
+ }
388
+ }
389
+ return result;
390
+ }
391
+ function getBrowserTimeZone() {
392
+ try {
393
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
394
+ if (typeof tz === "string" && tz.length > 0) return tz;
395
+ } catch {
396
+ }
397
+ return "UTC";
398
+ }
399
+ function eventWallToDisplay(eventWall, eventTimeZone, displayTimeZone) {
400
+ if (!eventTimeZone) return eventWall;
401
+ const target = displayTimeZone ?? getBrowserTimeZone();
402
+ if (eventTimeZone === target) return eventWall;
403
+ return convertWallTime(eventWall, eventTimeZone, target);
404
+ }
405
+ function displayWallToEvent(displayWall, eventTimeZone, displayTimeZone, options) {
406
+ if (!eventTimeZone) return displayWall;
407
+ const source = displayTimeZone ?? getBrowserTimeZone();
408
+ if (eventTimeZone === source) return displayWall;
409
+ return convertWallTime(displayWall, source, eventTimeZone, options);
410
+ }
411
+ var validTzCache = /* @__PURE__ */ new Map();
412
+ function isValidTimeZone(timeZone) {
413
+ if (typeof timeZone !== "string" || timeZone.length === 0) return false;
414
+ const cached = validTzCache.get(timeZone);
415
+ if (cached !== void 0) return cached;
416
+ let ok;
417
+ try {
418
+ Intl.DateTimeFormat("en-US", { timeZone });
419
+ ok = true;
420
+ } catch {
421
+ ok = false;
422
+ }
423
+ validTzCache.set(timeZone, ok);
424
+ return ok;
425
+ }
426
+ var FALLBACK_TIMEZONES = Object.freeze([
427
+ "UTC",
428
+ "Africa/Cairo",
429
+ "Africa/Johannesburg",
430
+ "Africa/Lagos",
431
+ "Africa/Nairobi",
432
+ "America/Anchorage",
433
+ "America/Argentina/Buenos_Aires",
434
+ "America/Bogota",
435
+ "America/Caracas",
436
+ "America/Chicago",
437
+ "America/Denver",
438
+ "America/Halifax",
439
+ "America/Lima",
440
+ "America/Los_Angeles",
441
+ "America/Mexico_City",
442
+ "America/Montevideo",
443
+ "America/New_York",
444
+ "America/Phoenix",
445
+ "America/Santiago",
446
+ "America/Sao_Paulo",
447
+ "America/St_Johns",
448
+ "America/Toronto",
449
+ "America/Vancouver",
450
+ "Asia/Bangkok",
451
+ "Asia/Dubai",
452
+ "Asia/Hong_Kong",
453
+ "Asia/Jakarta",
454
+ "Asia/Jerusalem",
455
+ "Asia/Karachi",
456
+ "Asia/Kathmandu",
457
+ "Asia/Kolkata",
458
+ "Asia/Kuala_Lumpur",
459
+ "Asia/Manila",
460
+ "Asia/Riyadh",
461
+ "Asia/Seoul",
462
+ "Asia/Shanghai",
463
+ "Asia/Singapore",
464
+ "Asia/Taipei",
465
+ "Asia/Tehran",
466
+ "Asia/Tokyo",
467
+ "Atlantic/Azores",
468
+ "Atlantic/Cape_Verde",
469
+ "Atlantic/Reykjavik",
470
+ "Australia/Adelaide",
471
+ "Australia/Brisbane",
472
+ "Australia/Darwin",
473
+ "Australia/Melbourne",
474
+ "Australia/Perth",
475
+ "Australia/Sydney",
476
+ "Europe/Amsterdam",
477
+ "Europe/Athens",
478
+ "Europe/Berlin",
479
+ "Europe/Brussels",
480
+ "Europe/Bucharest",
481
+ "Europe/Budapest",
482
+ "Europe/Copenhagen",
483
+ "Europe/Dublin",
484
+ "Europe/Helsinki",
485
+ "Europe/Istanbul",
486
+ "Europe/Lisbon",
487
+ "Europe/London",
488
+ "Europe/Madrid",
489
+ "Europe/Moscow",
490
+ "Europe/Oslo",
491
+ "Europe/Paris",
492
+ "Europe/Prague",
493
+ "Europe/Rome",
494
+ "Europe/Stockholm",
495
+ "Europe/Vienna",
496
+ "Europe/Warsaw",
497
+ "Europe/Zurich",
498
+ "Pacific/Auckland",
499
+ "Pacific/Fiji",
500
+ "Pacific/Guam",
501
+ "Pacific/Honolulu",
502
+ "Pacific/Midway",
503
+ "Pacific/Pago_Pago",
504
+ "Pacific/Tongatapu"
505
+ ]);
506
+ function listTimeZones() {
507
+ const fn = Intl.supportedValuesOf;
508
+ if (typeof fn === "function") {
509
+ try {
510
+ const values = fn("timeZone");
511
+ return values.includes("UTC") ? values.slice() : ["UTC", ...values];
512
+ } catch {
513
+ }
514
+ }
515
+ return FALLBACK_TIMEZONES.slice();
516
+ }
517
+ function getTimeZoneOffset(utcInstant, timeZone) {
518
+ const utcMs = parseUtcMs(utcInstant);
519
+ const wall = partsAt(timeZone, utcMs);
520
+ const wallAsUtcMs = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);
521
+ return Math.round((wallAsUtcMs - utcMs) / 6e4);
522
+ }
523
+ function wallTimeToUtc(wallTime, timeZone, options = {}) {
524
+ const ambiguous = options.ambiguous ?? "earlier";
525
+ const invalid = options.invalid ?? "shift";
526
+ const w = parseWall(wallTime);
527
+ const guessUtcMs = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second);
528
+ const offsetBefore = getTimeZoneOffset(utcMsToIso(guessUtcMs - 12 * 36e5), timeZone);
529
+ const offsetAfter = getTimeZoneOffset(utcMsToIso(guessUtcMs + 12 * 36e5), timeZone);
530
+ const candidateBefore = guessUtcMs - offsetBefore * 6e4;
531
+ const candidateAfter = guessUtcMs - offsetAfter * 6e4;
532
+ const roundTripBefore = roundTripWall(candidateBefore, timeZone);
533
+ const roundTripAfter = roundTripWall(candidateAfter, timeZone);
534
+ const beforeMatches = wallEquals(roundTripBefore, w);
535
+ const afterMatches = wallEquals(roundTripAfter, w);
536
+ if (beforeMatches && afterMatches) {
537
+ if (candidateBefore === candidateAfter) {
538
+ return utcMsToIso(candidateBefore);
539
+ }
540
+ return utcMsToIso(ambiguous === "earlier" ? Math.min(candidateBefore, candidateAfter) : Math.max(candidateBefore, candidateAfter));
541
+ }
542
+ if (beforeMatches) return utcMsToIso(candidateBefore);
543
+ if (afterMatches) return utcMsToIso(candidateAfter);
544
+ if (invalid === "throw") {
545
+ throw new RangeError(`Wall time ${wallTime} does not exist in ${timeZone} (DST gap)`);
546
+ }
547
+ return utcMsToIso(Math.max(candidateBefore, candidateAfter));
548
+ }
549
+ function roundTripWall(utcMs, timeZone) {
550
+ return partsAt(timeZone, utcMs);
551
+ }
552
+ function wallEquals(a, b) {
553
+ return a.year === b.year && a.month === b.month && a.day === b.day && a.hour === b.hour && a.minute === b.minute && a.second === b.second;
554
+ }
555
+ function utcToWallTime(utcInstant, timeZone) {
556
+ return formatWall(partsAt(timeZone, parseUtcMs(utcInstant)));
557
+ }
558
+ function convertWallTime(wallTime, fromTimeZone, toTimeZone, options) {
559
+ if (fromTimeZone === toTimeZone) return wallTime;
560
+ const utc = wallTimeToUtc(wallTime, fromTimeZone, options);
561
+ return utcToWallTime(utc, toTimeZone);
562
+ }
563
+ function getTimeZoneAbbreviation(timeZone, atInstant) {
564
+ const utcMs = atInstant === void 0 ? Date.now() : parseUtcMs(atInstant);
565
+ const dtf = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" });
566
+ const parts = dtf.formatToParts(new Date(utcMs));
567
+ const tzPart = parts.find((p) => p.type === "timeZoneName");
568
+ return tzPart?.value ?? timeZone;
569
+ }
570
+
279
571
  // src/utils/events.ts
280
572
  function sortEvents(events) {
281
573
  return [...events].sort((a, b) => {
@@ -300,14 +592,17 @@ function isMultiDayEvent(event) {
300
592
  function partitionEvents(events) {
301
593
  const allDay = [];
302
594
  const timed = [];
595
+ const background = [];
303
596
  for (const event of events) {
304
- if (event.allDay || isMultiDayEvent(event)) {
597
+ if (event.display === "background") {
598
+ background.push(event);
599
+ } else if (event.allDay || isMultiDayEvent(event)) {
305
600
  allDay.push(event);
306
601
  } else {
307
602
  timed.push(event);
308
603
  }
309
604
  }
310
- return { allDay, timed };
605
+ return { allDay, timed, background };
311
606
  }
312
607
  function segmentMultiDayEvent(event, rangeStart, rangeEnd) {
313
608
  const eventStartDate = event.start.slice(0, 10);
@@ -403,15 +698,17 @@ function assignColumns(group) {
403
698
  const totalColumns = columns.length;
404
699
  return result.map((r) => ({ ...r, totalColumns }));
405
700
  }
406
- function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24) {
701
+ function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24, displayTimeZone) {
407
702
  const totalHours = dayEndHour - dayStartHour;
408
703
  const groups = buildOverlapGroups(events);
409
704
  const positioned = [];
410
705
  for (const group of groups) {
411
706
  const columns = assignColumns(group);
412
707
  for (const { event, column, totalColumns } of columns) {
413
- const startTime = Math.max(getTimeOfDay(event.start), dayStartHour);
414
- const endTime = Math.min(getTimeOfDay(event.end), dayEndHour);
708
+ const eventStart = displayTimeZone && event.timeZone ? eventWallToDisplay(event.start, event.timeZone, displayTimeZone) : event.start;
709
+ const eventEnd = displayTimeZone && event.timeZone ? eventWallToDisplay(event.end, event.timeZone, displayTimeZone) : event.end;
710
+ const startTime = Math.max(getTimeOfDay(eventStart), dayStartHour);
711
+ const endTime = Math.min(getTimeOfDay(eventEnd), dayEndHour);
415
712
  const top = (startTime - dayStartHour) / totalHours * 100;
416
713
  const height = Math.max((endTime - startTime) / totalHours * 100, 1);
417
714
  positioned.push({
@@ -465,9 +762,9 @@ function yPositionToFractionalHour(clientY, columnRect, dayStart, dayEnd) {
465
762
  function normalizeRange(a, b) {
466
763
  return a <= b ? { start: a, end: b } : { start: b, end: a };
467
764
  }
468
- function computeDropPosition(day, clientY, columnRect, dayStartHour, dayEndHour, durationMs) {
765
+ function computeDropPosition(day, clientY, columnRect, dayStartHour, dayEndHour, durationMs, snapMinutes = 15) {
469
766
  const fractionalHour = yPositionToFractionalHour(clientY, columnRect, dayStartHour, dayEndHour);
470
- const snapped = snapToIncrement(fractionalHour, 15);
767
+ const snapped = snapToIncrement(fractionalHour, snapMinutes);
471
768
  const clamped = Math.max(dayStartHour, Math.min(dayEndHour - durationMs / 36e5, snapped));
472
769
  const newStart = fractionalHourToDateTime(day, clamped);
473
770
  const newStartMs = new Date(newStart).getTime();
@@ -787,6 +1084,191 @@ function scrollToViewportRange(scrollTop, containerHeight, totalHeight, dayStart
787
1084
  };
788
1085
  }
789
1086
 
1087
+ // src/utils/ical.ts
1088
+ function toICalDate(dateTime) {
1089
+ return dateTime.replace(/[-:]/g, "").replace("T", "T");
1090
+ }
1091
+ function escapeICalText(text) {
1092
+ return text.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\n/g, "\\n");
1093
+ }
1094
+ function eventsToICal(events, calendarName = "trud-calendar") {
1095
+ const lines = [
1096
+ "BEGIN:VCALENDAR",
1097
+ "VERSION:2.0",
1098
+ `PRODID:-//${calendarName}//EN`,
1099
+ "CALSCALE:GREGORIAN",
1100
+ "METHOD:PUBLISH"
1101
+ ];
1102
+ for (const event of events) {
1103
+ lines.push("BEGIN:VEVENT");
1104
+ lines.push(`UID:${event.id}`);
1105
+ if (event.allDay) {
1106
+ lines.push(`DTSTART;VALUE=DATE:${event.start.slice(0, 10).replace(/-/g, "")}`);
1107
+ lines.push(`DTEND;VALUE=DATE:${event.end.slice(0, 10).replace(/-/g, "")}`);
1108
+ } else {
1109
+ lines.push(`DTSTART:${toICalDate(event.start)}`);
1110
+ lines.push(`DTEND:${toICalDate(event.end)}`);
1111
+ }
1112
+ lines.push(`SUMMARY:${escapeICalText(event.title)}`);
1113
+ if (event.recurrence) {
1114
+ const parts = [`FREQ=${event.recurrence.freq.toUpperCase()}`];
1115
+ if (event.recurrence.interval && event.recurrence.interval > 1) {
1116
+ parts.push(`INTERVAL=${event.recurrence.interval}`);
1117
+ }
1118
+ if (event.recurrence.count) {
1119
+ parts.push(`COUNT=${event.recurrence.count}`);
1120
+ }
1121
+ if (event.recurrence.until) {
1122
+ parts.push(`UNTIL=${event.recurrence.until.replace(/-/g, "")}T235959`);
1123
+ }
1124
+ if (event.recurrence.byDay && event.recurrence.byDay.length > 0) {
1125
+ parts.push(`BYDAY=${event.recurrence.byDay.join(",")}`);
1126
+ }
1127
+ if (event.recurrence.byMonthDay && event.recurrence.byMonthDay.length > 0) {
1128
+ parts.push(`BYMONTHDAY=${event.recurrence.byMonthDay.join(",")}`);
1129
+ }
1130
+ if (event.recurrence.bySetPos) {
1131
+ parts.push(`BYSETPOS=${event.recurrence.bySetPos}`);
1132
+ }
1133
+ lines.push(`RRULE:${parts.join(";")}`);
1134
+ }
1135
+ if (event.exDates && event.exDates.length > 0) {
1136
+ const exDateValues = event.exDates.map((d) => d.replace(/-/g, "")).join(",");
1137
+ lines.push(`EXDATE;VALUE=DATE:${exDateValues}`);
1138
+ }
1139
+ lines.push("END:VEVENT");
1140
+ }
1141
+ lines.push("END:VCALENDAR");
1142
+ return lines.join("\r\n");
1143
+ }
1144
+ function downloadICal(events, filename = "calendar.ics", calendarName) {
1145
+ const content = eventsToICal(events, calendarName);
1146
+ const blob = new Blob([content], { type: "text/calendar;charset=utf-8" });
1147
+ const url = URL.createObjectURL(blob);
1148
+ const a = document.createElement("a");
1149
+ a.href = url;
1150
+ a.download = filename;
1151
+ a.click();
1152
+ URL.revokeObjectURL(url);
1153
+ }
1154
+
1155
+ // src/utils/resources.ts
1156
+ function flattenResources(resources) {
1157
+ const result = [];
1158
+ for (const resource of resources) {
1159
+ result.push(resource);
1160
+ if (resource.children && resource.children.length > 0) {
1161
+ result.push(...flattenResources(resource.children));
1162
+ }
1163
+ }
1164
+ return result;
1165
+ }
1166
+ function getEventsForResource(events, resourceId) {
1167
+ return events.filter((e) => e.resourceId === resourceId);
1168
+ }
1169
+ function groupEventsByResource(events, resources) {
1170
+ const map = /* @__PURE__ */ new Map();
1171
+ for (const resource of resources) {
1172
+ map.set(resource.id, []);
1173
+ }
1174
+ for (const event of events) {
1175
+ if (event.resourceId && map.has(event.resourceId)) {
1176
+ map.get(event.resourceId).push(event);
1177
+ }
1178
+ }
1179
+ return map;
1180
+ }
1181
+
1182
+ // src/utils/timeline.ts
1183
+ var datePart = (s) => s.slice(0, 10);
1184
+ function intersectDay(event, day) {
1185
+ const evStartDay = datePart(event.start);
1186
+ const evEndDay = datePart(event.end);
1187
+ if (day < evStartDay || day > evEndDay) return null;
1188
+ const startsToday = evStartDay === day;
1189
+ const endsToday = evEndDay === day;
1190
+ const start = startsToday ? getTimeOfDay(event.start) : 0;
1191
+ const end = endsToday ? getTimeOfDay(event.end) : 24;
1192
+ if (end <= start) return null;
1193
+ return { start, end, isStart: startsToday, isEnd: endsToday };
1194
+ }
1195
+ function computeTimelinePositions(events, resourceIds, day, dayStartHour = 0, dayEndHour = 24) {
1196
+ const totalHours = Math.max(1e-4, dayEndHour - dayStartHour);
1197
+ const result = /* @__PURE__ */ new Map();
1198
+ for (const id of resourceIds) result.set(id, []);
1199
+ const byResource = /* @__PURE__ */ new Map();
1200
+ for (const event of events) {
1201
+ if (event.allDay) continue;
1202
+ if (event.display === "background") continue;
1203
+ const rid = event.resourceId;
1204
+ if (!rid || !result.has(rid)) continue;
1205
+ let bucket = byResource.get(rid);
1206
+ if (!bucket) {
1207
+ bucket = [];
1208
+ byResource.set(rid, bucket);
1209
+ }
1210
+ bucket.push(event);
1211
+ }
1212
+ for (const [resourceId, bucketEvents] of byResource) {
1213
+ const clipped = [];
1214
+ for (const event of bucketEvents) {
1215
+ const window = intersectDay(event, day);
1216
+ if (!window) continue;
1217
+ const start = Math.max(window.start, dayStartHour);
1218
+ const end = Math.min(window.end, dayEndHour);
1219
+ if (end <= start) continue;
1220
+ clipped.push({
1221
+ original: event,
1222
+ clippedStart: start,
1223
+ clippedEnd: end,
1224
+ isSegmentStart: window.isStart && window.start >= dayStartHour,
1225
+ isSegmentEnd: window.isEnd && window.end <= dayEndHour
1226
+ });
1227
+ }
1228
+ if (clipped.length === 0) continue;
1229
+ const fakeIso = (h) => {
1230
+ const hours = Math.floor(h);
1231
+ const minutes = Math.floor((h - hours) * 60);
1232
+ const seconds = Math.floor(((h - hours) * 60 - minutes) * 60);
1233
+ const pad = (n) => n < 10 ? `0${n}` : `${n}`;
1234
+ return `${day}T${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
1235
+ };
1236
+ const synthetic = clipped.map((c, idx) => ({
1237
+ ...c.original,
1238
+ // Keep the original id but tag the index so two clipped segments
1239
+ // of the same parent never collide in a Map lookup.
1240
+ id: `${c.original.id}::tl::${idx}`,
1241
+ start: fakeIso(c.clippedStart),
1242
+ end: fakeIso(c.clippedEnd)
1243
+ }));
1244
+ const groups = buildOverlapGroups(synthetic);
1245
+ const positioned = [];
1246
+ for (const group of groups) {
1247
+ const cols = assignColumns(group);
1248
+ for (const { event, column, totalColumns } of cols) {
1249
+ const idx = parseInt(event.id.split("::tl::")[1] ?? "0", 10);
1250
+ const meta = clipped[idx];
1251
+ const start = meta.clippedStart;
1252
+ const end = meta.clippedEnd;
1253
+ const leftPct = (start - dayStartHour) / totalHours * 100;
1254
+ const widthPct = Math.max((end - start) / totalHours * 100, 0.5);
1255
+ positioned.push({
1256
+ event: meta.original,
1257
+ resourceId,
1258
+ leftPct,
1259
+ widthPct,
1260
+ row: column,
1261
+ totalRows: totalColumns,
1262
+ isSegmentStart: meta.isSegmentStart,
1263
+ isSegmentEnd: meta.isSegmentEnd
1264
+ });
1265
+ }
1266
+ }
1267
+ result.set(resourceId, positioned);
1268
+ }
1269
+ return result;
1270
+ }
1271
+
790
1272
  // src/utils/undo.ts
791
1273
  var DEFAULT_MAX_HISTORY = 30;
792
1274
  function createUndoStack(initial) {
@@ -839,6 +1321,7 @@ exports.DEFAULT_DAY_END_HOUR = DEFAULT_DAY_END_HOUR;
839
1321
  exports.DEFAULT_DAY_START_HOUR = DEFAULT_DAY_START_HOUR;
840
1322
  exports.DEFAULT_LABELS = DEFAULT_LABELS;
841
1323
  exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
1324
+ exports.DEFAULT_SNAP_DURATION = DEFAULT_SNAP_DURATION;
842
1325
  exports.DEFAULT_VIEW = DEFAULT_VIEW;
843
1326
  exports.HOURS_IN_DAY = HOURS_IN_DAY;
844
1327
  exports.MINUTES_IN_DAY = MINUTES_IN_DAY;
@@ -854,15 +1337,23 @@ exports.canRedo = canRedo;
854
1337
  exports.canUndo = canUndo;
855
1338
  exports.computeDropPosition = computeDropPosition;
856
1339
  exports.computeTimePositions = computeTimePositions;
1340
+ exports.computeTimelinePositions = computeTimelinePositions;
1341
+ exports.convertWallTime = convertWallTime;
857
1342
  exports.createInitialState = createInitialState;
858
1343
  exports.createUndoStack = createUndoStack;
859
1344
  exports.dateInRange = dateInRange;
860
1345
  exports.daysBetween = daysBetween;
1346
+ exports.displayWallToEvent = displayWallToEvent;
1347
+ exports.downloadICal = downloadICal;
861
1348
  exports.eachDayOfRange = eachDayOfRange;
862
1349
  exports.endOfMonth = endOfMonth;
1350
+ exports.eventWallToDisplay = eventWallToDisplay;
1351
+ exports.eventsToICal = eventsToICal;
863
1352
  exports.expandRecurringEvents = expandRecurringEvents;
864
1353
  exports.filterEventsInRange = filterEventsInRange;
1354
+ exports.filterHiddenDays = filterHiddenDays;
865
1355
  exports.filterVisibleEvents = filterVisibleEvents;
1356
+ exports.flattenResources = flattenResources;
866
1357
  exports.formatAgendaDate = formatAgendaDate;
867
1358
  exports.formatDayNumber = formatDayNumber;
868
1359
  exports.formatMonthDay = formatMonthDay;
@@ -873,22 +1364,30 @@ exports.formatWeekdayNarrow = formatWeekdayNarrow;
873
1364
  exports.formatWeekdayShort = formatWeekdayShort;
874
1365
  exports.fractionalHourToDateTime = fractionalHourToDateTime;
875
1366
  exports.generateOccurrences = generateOccurrences;
1367
+ exports.getBrowserTimeZone = getBrowserTimeZone;
876
1368
  exports.getDurationHours = getDurationHours;
877
1369
  exports.getEventSegments = getEventSegments;
878
1370
  exports.getEventsForDay = getEventsForDay;
1371
+ exports.getEventsForResource = getEventsForResource;
879
1372
  exports.getHourLabels = getHourLabels;
1373
+ exports.getISOWeekNumber = getISOWeekNumber;
880
1374
  exports.getMonthViewRange = getMonthViewRange;
881
1375
  exports.getTimeOfDay = getTimeOfDay;
1376
+ exports.getTimeZoneAbbreviation = getTimeZoneAbbreviation;
1377
+ exports.getTimeZoneOffset = getTimeZoneOffset;
882
1378
  exports.getVisibleRange = getVisibleRange;
883
1379
  exports.getWeekDays = getWeekDays;
884
1380
  exports.getWeekViewRange = getWeekViewRange;
885
1381
  exports.groupEventsByDate = groupEventsByDate;
1382
+ exports.groupEventsByResource = groupEventsByResource;
886
1383
  exports.isAfter = isAfter;
887
1384
  exports.isBefore = isBefore;
888
1385
  exports.isMultiDayEvent = isMultiDayEvent;
889
1386
  exports.isSameDay = isSameDay;
890
1387
  exports.isSameMonth = isSameMonth;
891
1388
  exports.isToday = isToday;
1389
+ exports.isValidTimeZone = isValidTimeZone;
1390
+ exports.listTimeZones = listTimeZones;
892
1391
  exports.normalizeRange = normalizeRange;
893
1392
  exports.parseDate = parseDate;
894
1393
  exports.parseRRule = parseRRule;
@@ -907,6 +1406,8 @@ exports.toDateString = toDateString;
907
1406
  exports.toDateTimeString = toDateTimeString;
908
1407
  exports.toRRuleString = toRRuleString;
909
1408
  exports.undo = undo;
1409
+ exports.utcToWallTime = utcToWallTime;
1410
+ exports.wallTimeToUtc = wallTimeToUtc;
910
1411
  exports.yPositionToFractionalHour = yPositionToFractionalHour;
911
1412
  //# sourceMappingURL=index.cjs.map
912
1413
  //# sourceMappingURL=index.cjs.map