trud-calendar-core 0.4.0 → 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
@@ -8,6 +8,7 @@ var DEFAULT_LABELS = {
8
8
  day: "Day",
9
9
  agenda: "Agenda",
10
10
  year: "Year",
11
+ timeline: "Timeline",
11
12
  allDay: "all-day",
12
13
  noEvents: "No events in this period",
13
14
  more: (n) => `+${n} more`
@@ -20,7 +21,7 @@ var DEFAULT_VIEW = "month";
20
21
  var HOURS_IN_DAY = 24;
21
22
  var MINUTES_IN_HOUR = 60;
22
23
  var MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
23
- var VIEWS = ["month", "week", "day", "agenda", "year"];
24
+ var VIEWS = ["month", "week", "day", "agenda", "year", "timeline"];
24
25
  var DEFAULT_DAY_START_HOUR = 0;
25
26
  var DEFAULT_DAY_END_HOUR = 24;
26
27
  var DEFAULT_SNAP_DURATION = 15;
@@ -142,6 +143,8 @@ function getVisibleRange(date, view, weekStartsOn = 0) {
142
143
  const yearEnd = `${d.getFullYear()}-12-31`;
143
144
  return { start: yearStart, end: yearEnd };
144
145
  }
146
+ case "timeline":
147
+ return { start: date, end: date };
145
148
  }
146
149
  }
147
150
  function getISOWeekNumber(date) {
@@ -215,6 +218,13 @@ function formatToolbarTitle(date, view, locale = "en-US") {
215
218
  return getFormatter(locale, { month: "long", year: "numeric" }).format(d);
216
219
  case "year":
217
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);
218
228
  }
219
229
  }
220
230
  function formatWeekdayShort(date, locale = "en-US") {
@@ -269,6 +279,8 @@ function navigateByView(date, view, direction) {
269
279
  return addMonths(date, direction);
270
280
  case "year":
271
281
  return addMonths(date, 12 * direction);
282
+ case "timeline":
283
+ return addDays(date, direction);
272
284
  }
273
285
  }
274
286
  function calendarReducer(state, action) {
@@ -301,6 +313,261 @@ function calendarReducer(state, action) {
301
313
  }
302
314
  }
303
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
+
304
571
  // src/utils/events.ts
305
572
  function sortEvents(events) {
306
573
  return [...events].sort((a, b) => {
@@ -431,15 +698,17 @@ function assignColumns(group) {
431
698
  const totalColumns = columns.length;
432
699
  return result.map((r) => ({ ...r, totalColumns }));
433
700
  }
434
- function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24) {
701
+ function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24, displayTimeZone) {
435
702
  const totalHours = dayEndHour - dayStartHour;
436
703
  const groups = buildOverlapGroups(events);
437
704
  const positioned = [];
438
705
  for (const group of groups) {
439
706
  const columns = assignColumns(group);
440
707
  for (const { event, column, totalColumns } of columns) {
441
- const startTime = Math.max(getTimeOfDay(event.start), dayStartHour);
442
- 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);
443
712
  const top = (startTime - dayStartHour) / totalHours * 100;
444
713
  const height = Math.max((endTime - startTime) / totalHours * 100, 1);
445
714
  positioned.push({
@@ -910,6 +1179,96 @@ function groupEventsByResource(events, resources) {
910
1179
  return map;
911
1180
  }
912
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
+
913
1272
  // src/utils/undo.ts
914
1273
  var DEFAULT_MAX_HISTORY = 30;
915
1274
  function createUndoStack(initial) {
@@ -978,13 +1337,17 @@ exports.canRedo = canRedo;
978
1337
  exports.canUndo = canUndo;
979
1338
  exports.computeDropPosition = computeDropPosition;
980
1339
  exports.computeTimePositions = computeTimePositions;
1340
+ exports.computeTimelinePositions = computeTimelinePositions;
1341
+ exports.convertWallTime = convertWallTime;
981
1342
  exports.createInitialState = createInitialState;
982
1343
  exports.createUndoStack = createUndoStack;
983
1344
  exports.dateInRange = dateInRange;
984
1345
  exports.daysBetween = daysBetween;
1346
+ exports.displayWallToEvent = displayWallToEvent;
985
1347
  exports.downloadICal = downloadICal;
986
1348
  exports.eachDayOfRange = eachDayOfRange;
987
1349
  exports.endOfMonth = endOfMonth;
1350
+ exports.eventWallToDisplay = eventWallToDisplay;
988
1351
  exports.eventsToICal = eventsToICal;
989
1352
  exports.expandRecurringEvents = expandRecurringEvents;
990
1353
  exports.filterEventsInRange = filterEventsInRange;
@@ -1001,6 +1364,7 @@ exports.formatWeekdayNarrow = formatWeekdayNarrow;
1001
1364
  exports.formatWeekdayShort = formatWeekdayShort;
1002
1365
  exports.fractionalHourToDateTime = fractionalHourToDateTime;
1003
1366
  exports.generateOccurrences = generateOccurrences;
1367
+ exports.getBrowserTimeZone = getBrowserTimeZone;
1004
1368
  exports.getDurationHours = getDurationHours;
1005
1369
  exports.getEventSegments = getEventSegments;
1006
1370
  exports.getEventsForDay = getEventsForDay;
@@ -1009,6 +1373,8 @@ exports.getHourLabels = getHourLabels;
1009
1373
  exports.getISOWeekNumber = getISOWeekNumber;
1010
1374
  exports.getMonthViewRange = getMonthViewRange;
1011
1375
  exports.getTimeOfDay = getTimeOfDay;
1376
+ exports.getTimeZoneAbbreviation = getTimeZoneAbbreviation;
1377
+ exports.getTimeZoneOffset = getTimeZoneOffset;
1012
1378
  exports.getVisibleRange = getVisibleRange;
1013
1379
  exports.getWeekDays = getWeekDays;
1014
1380
  exports.getWeekViewRange = getWeekViewRange;
@@ -1020,6 +1386,8 @@ exports.isMultiDayEvent = isMultiDayEvent;
1020
1386
  exports.isSameDay = isSameDay;
1021
1387
  exports.isSameMonth = isSameMonth;
1022
1388
  exports.isToday = isToday;
1389
+ exports.isValidTimeZone = isValidTimeZone;
1390
+ exports.listTimeZones = listTimeZones;
1023
1391
  exports.normalizeRange = normalizeRange;
1024
1392
  exports.parseDate = parseDate;
1025
1393
  exports.parseRRule = parseRRule;
@@ -1038,6 +1406,8 @@ exports.toDateString = toDateString;
1038
1406
  exports.toDateTimeString = toDateTimeString;
1039
1407
  exports.toRRuleString = toRRuleString;
1040
1408
  exports.undo = undo;
1409
+ exports.utcToWallTime = utcToWallTime;
1410
+ exports.wallTimeToUtc = wallTimeToUtc;
1041
1411
  exports.yPositionToFractionalHour = yPositionToFractionalHour;
1042
1412
  //# sourceMappingURL=index.cjs.map
1043
1413
  //# sourceMappingURL=index.cjs.map