@luckydye/calendar 1.1.3 → 1.2.1

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.
@@ -241,6 +241,10 @@ export class CalendarViewElement extends LitElement {
241
241
  overflow-anchor: none;
242
242
  }
243
243
 
244
+ :host([scroll-lock]) .scroll-container {
245
+ overflow: hidden;
246
+ }
247
+
244
248
  .scroll-container.zoom-cursor {
245
249
  cursor: ns-resize;
246
250
  }
@@ -330,12 +334,11 @@ export class CalendarViewElement extends LitElement {
330
334
  font-size: 16px;
331
335
  font-weight: 600;
332
336
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
333
- background: var(--input-bg, rgba(0, 0, 0, 0.2));
334
337
  border: 1px solid var(--input-border, rgba(255, 255, 255, 0.1));
338
+ background: transparent;
335
339
  border-radius: 4px;
336
340
  padding: 6px 8px;
337
341
  width: 100%;
338
- outline: none;
339
342
  font-family: inherit;
340
343
  margin: 0 -8px 0 -8px;
341
344
  field-sizing: content;
@@ -356,7 +359,6 @@ export class CalendarViewElement extends LitElement {
356
359
 
357
360
  .event-detail-title-input:focus {
358
361
  border-color: var(--input-border-focus, rgba(255, 255, 255, 0.3));
359
- background: var(--input-bg-focus, rgba(0, 0, 0, 0.3));
360
362
  }
361
363
 
362
364
  .event-detail-time {
@@ -377,6 +379,9 @@ export class CalendarViewElement extends LitElement {
377
379
  font-size: 20px;
378
380
  line-height: 1;
379
381
  flex-shrink: 0;
382
+ position: absolute;
383
+ top: 0.75rem;
384
+ right: 1rem;
380
385
  }
381
386
 
382
387
  .event-detail-close:hover {
@@ -745,19 +750,91 @@ export class CalendarViewElement extends LitElement {
745
750
  return this._scrollTop;
746
751
  }
747
752
 
748
- setView(dayHeight: number, scrollTop: number, syncScrollDOM = true): void {
749
- this._dayHeight = dayHeight;
750
- this._scrollTop = scrollTop;
753
+ setView(
754
+ toDayHeight: number,
755
+ toScrollTop: number,
756
+ syncScrollDOM = true,
757
+ animate = false,
758
+ ): void {
759
+ this.cancelScrollAnimation();
760
+
761
+ if (animate) {
762
+ const startDayHeight = this._dayHeight;
763
+ const startScroll = this._scrollTop;
764
+
765
+ // Compute scroll as a fraction of total height at each zoom level for smooth interpolation
766
+ const startTotal = this.totalHeight;
767
+
768
+ this._dayHeight = toDayHeight;
769
+ this.updateWeekOffsets();
770
+ const targetTotal = this.totalHeight;
771
+
772
+ this._dayHeight = startDayHeight;
773
+ this.updateWeekOffsets();
774
+
775
+ const startFrac = startTotal > 0 ? startScroll / startTotal : 0;
776
+ const targetFrac = targetTotal > 0 ? toScrollTop / targetTotal : 0;
777
+
778
+ const duration = 600;
779
+ const startTime = performance.now();
780
+ const easeOutExpo = (t: number): number =>
781
+ t === 1 ? 1 : 1 - 2 ** (-10 * t);
782
+ const easeOutCubic = (t: number): number => 1 - (1 - t) ** 3;
783
+
784
+ const tick = (currentTime: number): void => {
785
+ if (!this.scrollAnimationFrame) return;
786
+
787
+ const progress = Math.min((currentTime - startTime) / duration, 1);
788
+
789
+ this._dayHeight = startDayHeight + (toDayHeight - startDayHeight) * easeOutExpo(progress);
790
+ this.updateWeekOffsets();
791
+
792
+ const currentFrac = startFrac + (targetFrac - startFrac) * easeOutCubic(progress);
793
+ this._scrollTop = Math.max(0, currentFrac * this.totalHeight);
794
+
795
+ if (this.scrollContainer) {
796
+ if (
797
+ this.scrollContent &&
798
+ this.scrollContainer.scrollHeight < this._scrollTop
799
+ ) {
800
+ this.scrollContent.style.minHeight =
801
+ this._scrollTop + window.innerHeight + "px";
802
+ }
803
+ this.scrollContainer.scrollTop = this._scrollTop;
804
+ }
805
+
806
+ this.renderCanvas();
807
+
808
+ if (progress < 1) {
809
+ this.scrollAnimationFrame = requestAnimationFrame(tick);
810
+ } else {
811
+ this._dayHeight = toDayHeight;
812
+ this._scrollTop = toScrollTop;
813
+ this.saveDayHeight();
814
+ this.saveScrollPosition();
815
+ this.scrollAnimationFrame = null;
816
+ this.renderCanvas();
817
+ }
818
+ };
819
+
820
+ this.scrollAnimationFrame = requestAnimationFrame(tick);
821
+
822
+ return;
823
+ }
824
+
825
+ this._dayHeight = toDayHeight;
826
+ this._scrollTop = toScrollTop;
751
827
 
752
828
  this.saveDayHeight();
753
829
  this.updateWeekOffsets();
754
830
 
755
831
  if (syncScrollDOM && this.scrollContainer && this.scrollContent) {
756
- if (this.scrollContainer.scrollHeight < scrollTop) {
757
- this.scrollContent.style.minHeight =
758
- scrollTop + window.innerHeight + "px";
832
+ if (this.scrollContainer.scrollHeight < toScrollTop) {
833
+ this.scrollContent.style.minHeight = `${
834
+ toScrollTop + window.innerHeight
835
+ }px`;
759
836
  }
760
- this.scrollContainer.scrollTop = scrollTop;
837
+ this.scrollContainer.scrollTop = toScrollTop;
761
838
  }
762
839
 
763
840
  this.saveScrollPosition();
@@ -851,17 +928,19 @@ export class CalendarViewElement extends LitElement {
851
928
  return { row, col };
852
929
  }
853
930
 
854
- getVisualPositionFromCoords(x: number, y: number, gridWidth: number): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
931
+ getVisualPositionFromCoords(
932
+ x: number,
933
+ y: number,
934
+ gridWidth: number,
935
+ ): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
855
936
  if (!this.scrollContainer) return null;
856
-
937
+
857
938
  const dayWidth = gridWidth / this._columnsPerRow;
858
939
  const col = Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth);
859
940
  if (col < 0 || col >= this._columnsPerRow) return null;
860
941
 
861
- const week = this.weeks.find(w =>
862
- w.height > 0 &&
863
- y >= w.yOffset &&
864
- y < w.yOffset + w.height
942
+ const week = this.weeks.find(
943
+ (w) => w.height > 0 && y >= w.yOffset && y < w.yOffset + w.height,
865
944
  );
866
945
  if (!week) return null;
867
946
 
@@ -933,8 +1012,15 @@ export class CalendarViewElement extends LitElement {
933
1012
  this.internal = new CalendarInternal({
934
1013
  locale: this.getAttribute("locale") || undefined,
935
1014
  weekStart: Number(this.getAttribute("week-start")),
936
- storage: new IndexedDBStorage()
1015
+ storage: new IndexedDBStorage(),
937
1016
  });
1017
+
1018
+ this.addEventListener("wheel", this.onWheel, { passive: false });
1019
+ this.addEventListener("dragstart", this.onDragStart);
1020
+ this.addEventListener("dragend", this.onDragEnd);
1021
+ this.addEventListener("dragover", this.onDragOver);
1022
+ this.addEventListener("dragleave", this.onDragLeave);
1023
+ this.addEventListener("drop", this.onDrop);
938
1024
  }
939
1025
 
940
1026
  loadDayHeight(): number {
@@ -984,18 +1070,16 @@ export class CalendarViewElement extends LitElement {
984
1070
  }
985
1071
  }
986
1072
 
987
- scrollToToday = (): void => {
1073
+ scrollToToday = (animate = true): void => {
988
1074
  const now = new Date();
989
1075
 
990
- // Ensure today is in range before computing offset
991
- let weekIndex = this.weeks.findIndex(
992
- (w) => w.days.some((d) => CalendarInternal.isSameDay(d, now)),
1076
+ let weekIndex = this.weeks.findIndex((w) =>
1077
+ w.days.some((d) => CalendarInternal.isSameDay(d, now)),
993
1078
  );
994
1079
  if (weekIndex < 0) {
995
1080
  this.weeks = this.internal.resetRangeAroundDate(now);
996
- this.updateWeekOffsets();
997
- weekIndex = this.weeks.findIndex(
998
- (w) => w.days.some((d) => CalendarInternal.isSameDay(d, now)),
1081
+ weekIndex = this.weeks.findIndex((w) =>
1082
+ w.days.some((d) => CalendarInternal.isSameDay(d, now)),
999
1083
  );
1000
1084
  }
1001
1085
 
@@ -1012,7 +1096,17 @@ export class CalendarViewElement extends LitElement {
1012
1096
  }
1013
1097
  }
1014
1098
 
1015
- this.scrollToDate(now, offsetInWeek, true, true);
1099
+ this.scrollToDate(now, offsetInWeek, animate, true, 900);
1100
+ };
1101
+
1102
+ scrollToMonth = (animate = true): void => {
1103
+ const now = new Date();
1104
+ const monthMid = new Date(now.getFullYear(), now.getMonth(), 15);
1105
+ const dayHeight = Math.max(
1106
+ MIN_DAY_HEIGHT,
1107
+ Math.round(this.viewportHeight / 5),
1108
+ );
1109
+ this.scrollToDate(monthMid, 0.5, animate, true, dayHeight);
1016
1110
  };
1017
1111
 
1018
1112
  // History management methods
@@ -1100,7 +1194,10 @@ export class CalendarViewElement extends LitElement {
1100
1194
  offsetInWeek = 0.5,
1101
1195
  animate = false,
1102
1196
  extendRange = false,
1197
+ targetDayHeight?: number,
1103
1198
  ): void {
1199
+ const dayHeight = targetDayHeight ?? this._dayHeight;
1200
+
1104
1201
  let weekIndex = this.weeks.findIndex(
1105
1202
  (w) =>
1106
1203
  w.days.some((d) => CalendarInternal.isSameDay(d, targetDate)) ||
@@ -1113,7 +1210,6 @@ export class CalendarViewElement extends LitElement {
1113
1210
  // If date not found in current buffer, reset range around target (only if explicitly requested)
1114
1211
  if (weekIndex < 0 && extendRange) {
1115
1212
  this.weeks = this.internal.resetRangeAroundDate(targetDate);
1116
- this.updateWeekOffsets();
1117
1213
 
1118
1214
  // Find the week again in the new range
1119
1215
  weekIndex = this.weeks.findIndex(
@@ -1127,15 +1223,24 @@ export class CalendarViewElement extends LitElement {
1127
1223
  }
1128
1224
 
1129
1225
  if (weekIndex >= 0) {
1226
+ // Temporarily apply target dayHeight to compute correct week offsets
1227
+ const savedDayHeight = this._dayHeight;
1228
+ this._dayHeight = dayHeight;
1229
+ this.updateWeekOffsets();
1230
+
1130
1231
  const targetWeek = this.weeks[weekIndex];
1131
1232
  if (targetWeek) {
1132
1233
  const targetY = targetWeek.yOffset + targetWeek.height * offsetInWeek;
1133
1234
  const targetScroll = Math.max(0, targetY - this.viewportHeight / 2);
1134
- if (animate) {
1135
- this.animateScrollTo(targetScroll);
1136
- } else {
1137
- this.scrollTop = targetScroll;
1138
- }
1235
+
1236
+ // Restore so animateToView reads the correct start dayHeight
1237
+ this._dayHeight = savedDayHeight;
1238
+ if (animate) this.updateWeekOffsets();
1239
+
1240
+ this.setView(dayHeight, targetScroll, true, animate);
1241
+ } else {
1242
+ this._dayHeight = savedDayHeight;
1243
+ this.updateWeekOffsets();
1139
1244
  }
1140
1245
  }
1141
1246
  }
@@ -1147,39 +1252,6 @@ export class CalendarViewElement extends LitElement {
1147
1252
  }
1148
1253
  };
1149
1254
 
1150
- private animateScrollTo(targetScroll: number): void {
1151
- this.cancelScrollAnimation();
1152
-
1153
- if (!this.scrollContainer) return;
1154
-
1155
- const startScroll = this.scrollTop;
1156
- const distance = targetScroll - startScroll;
1157
- const duration = 800;
1158
- const startTime = performance.now();
1159
-
1160
- const easeOutExpo = (t: number): number => {
1161
- return t === 1 ? 1 : 1 - 2 ** (-10 * t);
1162
- };
1163
-
1164
- const animate = (currentTime: number): void => {
1165
- if (!this.scrollAnimationFrame) return;
1166
-
1167
- const elapsed = currentTime - startTime;
1168
- const progress = Math.min(elapsed / duration, 1);
1169
- const easedProgress = easeOutExpo(progress);
1170
-
1171
- this.scrollTop = startScroll + distance * easedProgress;
1172
-
1173
- if (progress < 1) {
1174
- this.scrollAnimationFrame = requestAnimationFrame(animate);
1175
- } else {
1176
- this.scrollAnimationFrame = null;
1177
- }
1178
- };
1179
-
1180
- this.scrollAnimationFrame = requestAnimationFrame(animate);
1181
- }
1182
-
1183
1255
  // Push current state when entering filter mode
1184
1256
  private pushFilterToHistory(): void {
1185
1257
  if (this.saveHistoryTimeout) {
@@ -1234,25 +1306,16 @@ export class CalendarViewElement extends LitElement {
1234
1306
 
1235
1307
  window.addEventListener("mousemove", this.onMouseMove);
1236
1308
  window.addEventListener("mouseup", this.onMouseUp);
1237
- window.addEventListener("wheel", this.onWheel, { passive: false });
1238
1309
  window.addEventListener("paste", this.onPaste);
1239
- this.addEventListener("dragstart", this.onDragStart);
1240
- this.addEventListener("dragend", this.onDragEnd);
1241
- this.addEventListener("dragover", this.onDragOver);
1242
- this.addEventListener("dragleave", this.onDragLeave);
1243
- this.addEventListener("drop", this.onDrop);
1244
1310
  }
1245
1311
 
1246
1312
  disconnectedCallback() {
1247
1313
  super.disconnectedCallback();
1248
- document.removeEventListener("mousemove", this.onMouseMove);
1249
- document.removeEventListener("mouseup", this.onMouseUp);
1314
+
1315
+ window.removeEventListener("mousemove", this.onMouseMove);
1316
+ window.removeEventListener("mouseup", this.onMouseUp);
1250
1317
  window.removeEventListener("paste", this.onPaste);
1251
- this.removeEventListener("dragstart", this.onDragStart);
1252
- this.removeEventListener("dragend", this.onDragEnd);
1253
- this.removeEventListener("dragover", this.onDragOver);
1254
- this.removeEventListener("dragleave", this.onDragLeave);
1255
- this.removeEventListener("drop", this.onDrop);
1318
+
1256
1319
  if (this.scrollContainer) {
1257
1320
  this.scrollContainer.removeEventListener(
1258
1321
  "mouseleave",
@@ -1277,6 +1340,25 @@ export class CalendarViewElement extends LitElement {
1277
1340
 
1278
1341
  updated() {
1279
1342
  this.handleResize();
1343
+ this.repositionEventDetailOverlay();
1344
+ }
1345
+
1346
+ repositionEventDetailOverlay(): void {
1347
+ if (!this.selectedEventForDetail || !this.selectedEventRect) return;
1348
+
1349
+ const overlay = this.renderRoot.querySelector<HTMLElement>(".event-detail-overlay");
1350
+ if (!overlay) return;
1351
+
1352
+ const actualHeight = overlay.offsetHeight;
1353
+ const GAP = 8;
1354
+ const containerHeight = this.scrollContainer?.clientHeight || 600;
1355
+
1356
+ const rawTop = this.selectedEventRect.y - this.scrollTop;
1357
+ const minTop = GAP;
1358
+ const maxTop = containerHeight - actualHeight - GAP;
1359
+ const top = Math.max(minTop, Math.min(maxTop, rawTop));
1360
+
1361
+ overlay.style.top = `${top}px`;
1280
1362
  }
1281
1363
 
1282
1364
  async firstUpdated() {
@@ -1299,7 +1381,9 @@ export class CalendarViewElement extends LitElement {
1299
1381
  }
1300
1382
 
1301
1383
  if (this.scrollContainer) {
1302
- this.scrollContainer.addEventListener("scroll", this.onScroll);
1384
+ this.scrollContainer.addEventListener("scroll", this.onScroll, {
1385
+ passive: false,
1386
+ });
1303
1387
  this.scrollContainer.addEventListener(
1304
1388
  "mouseleave",
1305
1389
  this.onScrollContainerMouseLeave,
@@ -1451,15 +1535,18 @@ export class CalendarViewElement extends LitElement {
1451
1535
 
1452
1536
  updateColumnsForViewport(): void {
1453
1537
  if (!this.scrollContainer) return;
1454
-
1538
+
1455
1539
  const rect = this.scrollContainer.getBoundingClientRect();
1456
1540
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
1457
-
1541
+
1458
1542
  // Determine optimal columns based on available width
1459
1543
  // Minimum 60px per day column for usability
1460
1544
  const minDayWidth = 120;
1461
- const optimalColumns = Math.max(1, Math.min(7, Math.floor(gridWidth / minDayWidth)));
1462
-
1545
+ const optimalColumns = Math.max(
1546
+ 1,
1547
+ Math.min(7, Math.floor(gridWidth / minDayWidth)),
1548
+ );
1549
+
1463
1550
  if (this._columnsPerRow !== optimalColumns) {
1464
1551
  this._columnsPerRow = optimalColumns;
1465
1552
  this.updateWeekOffsets();
@@ -1685,7 +1772,8 @@ export class CalendarViewElement extends LitElement {
1685
1772
  if (todayIndex >= 0) {
1686
1773
  const { row } = this.getDayVisualPosition(todayIndex);
1687
1774
  const currentMinutes = today.getHours() * 60 + today.getMinutes();
1688
- const timeY = y + row * this.dayHeight + (currentMinutes / 1440) * this.dayHeight;
1775
+ const timeY =
1776
+ y + row * this.dayHeight + (currentMinutes / 1440) * this.dayHeight;
1689
1777
 
1690
1778
  if (timeY >= 0 && timeY <= height) {
1691
1779
  const hours = today.getHours().toString().padStart(2, "0");
@@ -1718,7 +1806,10 @@ export class CalendarViewElement extends LitElement {
1718
1806
  ctx.fill();
1719
1807
 
1720
1808
  // Draw current time text in white
1721
- ctx.fillStyle = "white";
1809
+ ctx.fillStyle =
1810
+ getComputedStyle(this)
1811
+ .getPropertyValue("--text-primary")
1812
+ .trim() || "rgba(255, 255, 255, 1)";
1722
1813
  ctx.fillText(timeText, textX, textY);
1723
1814
  ctx.restore();
1724
1815
  }
@@ -1762,10 +1853,10 @@ export class CalendarViewElement extends LitElement {
1762
1853
  this.renderEventsOnCanvas(ctx, scrollTop, height, dayWidth, visibleWeeks);
1763
1854
 
1764
1855
  this.renderDateLabels();
1765
-
1856
+
1766
1857
  // Draw sticky weekday labels at top of viewport
1767
1858
  this.renderWeekdayLabels(ctx, dayWidth, visibleWeeks, scrollTop, height);
1768
-
1859
+
1769
1860
  if (this.isCreatingEvent) {
1770
1861
  this.renderEventCreationPreview();
1771
1862
  }
@@ -1895,7 +1986,7 @@ export class CalendarViewElement extends LitElement {
1895
1986
  });
1896
1987
  }
1897
1988
  return result;
1898
- })
1989
+ })
1899
1990
  : timedSegments;
1900
1991
 
1901
1992
  // Combine segments with all-day events first (so they get top rows)
@@ -2577,6 +2668,8 @@ export class CalendarViewElement extends LitElement {
2577
2668
  }
2578
2669
 
2579
2670
  onWheel = (e: WheelEvent): void => {
2671
+ if (this.hasAttribute("scroll-lock")) return;
2672
+
2580
2673
  if (this.scrollAnimationFrame) {
2581
2674
  this.cancelScrollAnimation();
2582
2675
  }
@@ -3077,7 +3170,9 @@ export class CalendarViewElement extends LitElement {
3077
3170
  const item = new ClipboardItem({ "text/plain": blob });
3078
3171
  navigator.clipboard.write([item]).then(() => {
3079
3172
  const count = selectedEvents.length;
3080
- queueStatus(`Copied ${count} event${count === 1 ? "" : "s"} to clipboard`);
3173
+ queueStatus(
3174
+ `Copied ${count} event${count === 1 ? "" : "s"} to clipboard`,
3175
+ );
3081
3176
  });
3082
3177
  }
3083
3178
 
@@ -3594,11 +3689,17 @@ export class CalendarViewElement extends LitElement {
3594
3689
  // Find column indices from X coordinates
3595
3690
  const startCol = Math.max(
3596
3691
  0,
3597
- Math.min(this._columnsPerRow - 1, Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth)),
3692
+ Math.min(
3693
+ this._columnsPerRow - 1,
3694
+ Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth),
3695
+ ),
3598
3696
  );
3599
3697
  const endCol = Math.max(
3600
3698
  0,
3601
- Math.min(this._columnsPerRow - 1, Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth)),
3699
+ Math.min(
3700
+ this._columnsPerRow - 1,
3701
+ Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth),
3702
+ ),
3602
3703
  );
3603
3704
 
3604
3705
  // Find all weeks that intersect with the selection box
@@ -3620,19 +3721,19 @@ export class CalendarViewElement extends LitElement {
3620
3721
  for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
3621
3722
  const day = week.days[dayIndex];
3622
3723
  if (!day) continue;
3623
-
3724
+
3624
3725
  const { row, col } = this.getDayVisualPosition(dayIndex);
3625
-
3726
+
3626
3727
  // Check if this day's column is in the selection
3627
3728
  if (col < startCol || col > endCol) continue;
3628
-
3729
+
3629
3730
  // Calculate Y bounds for this visual row
3630
3731
  const rowTop = week.yOffset + row * this.dayHeight;
3631
3732
  const rowBottom = rowTop + this.dayHeight;
3632
-
3733
+
3633
3734
  // Check if selection intersects with this row
3634
3735
  if (maxY < rowTop || minY > rowBottom) continue;
3635
-
3736
+
3636
3737
  const dayMinY = Math.max(minY, rowTop);
3637
3738
  const dayMaxY = Math.min(maxY, rowBottom);
3638
3739
 
@@ -3646,7 +3747,9 @@ export class CalendarViewElement extends LitElement {
3646
3747
 
3647
3748
  // Calculate end time for this day
3648
3749
  const dayEndOffset = dayMaxY - rowTop;
3649
- const endMinutes = Math.ceil((dayEndOffset / this.dayHeight) * 24 * 60);
3750
+ const endMinutes = Math.ceil(
3751
+ (dayEndOffset / this.dayHeight) * 24 * 60,
3752
+ );
3650
3753
  const endHour = Math.floor(endMinutes / 60);
3651
3754
  const endMinute = endMinutes % 60;
3652
3755
 
@@ -3667,18 +3770,18 @@ export class CalendarViewElement extends LitElement {
3667
3770
  for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
3668
3771
  const day = week.days[dayIndex];
3669
3772
  if (!day) continue;
3670
-
3773
+
3671
3774
  const { row, col } = this.getDayVisualPosition(dayIndex);
3672
-
3775
+
3673
3776
  // Check if this day's column is in the selection
3674
3777
  if (col < startCol || col > endCol) continue;
3675
-
3778
+
3676
3779
  // Check if this day's row is in the selection
3677
3780
  const rowTop = week.yOffset + row * this.dayHeight;
3678
3781
  const rowBottom = rowTop + this.dayHeight;
3679
-
3782
+
3680
3783
  if (maxY < rowTop || minY > rowBottom) continue;
3681
-
3784
+
3682
3785
  const rangeStart = new Date(day);
3683
3786
  rangeStart.setHours(0, 0, 0, 0);
3684
3787
 
@@ -3764,6 +3867,8 @@ export class CalendarViewElement extends LitElement {
3764
3867
 
3765
3868
  this.renderCanvas();
3766
3869
 
3870
+ this.repositionEventDetailOverlay();
3871
+
3767
3872
  this.checkAndExtendRange();
3768
3873
 
3769
3874
  this.saveScrollPosition();
@@ -3867,7 +3972,9 @@ export class CalendarViewElement extends LitElement {
3867
3972
  onNotificationPopoverToggle = async (): Promise<void> => {
3868
3973
  this.notificationPopoverOpen = !this.notificationPopoverOpen;
3869
3974
  if (this.notificationPopoverOpen) {
3870
- this.dispatchEvent(new CustomEvent("load-notifications", { bubbles: true }));
3975
+ this.dispatchEvent(
3976
+ new CustomEvent("load-notifications", { bubbles: true }),
3977
+ );
3871
3978
  }
3872
3979
  this.requestUpdate();
3873
3980
  };
@@ -3877,7 +3984,6 @@ export class CalendarViewElement extends LitElement {
3877
3984
  this.requestUpdate();
3878
3985
  };
3879
3986
 
3880
-
3881
3987
  onThemeChange = (e: Event): void => {
3882
3988
  const select = e.target as HTMLSelectElement;
3883
3989
  const theme = select.value as ThemeName;
@@ -3912,8 +4018,6 @@ export class CalendarViewElement extends LitElement {
3912
4018
  async onEventClick(event: CalendarEvent, e: MouseEvent): Promise<void> {
3913
4019
  const isCmdOrCtrl = e.metaKey || e.ctrlKey;
3914
4020
 
3915
- console.log(event);
3916
-
3917
4021
  if (isCmdOrCtrl) {
3918
4022
  this.internal.selectEvent(event, "toggle");
3919
4023
  } else {
@@ -4204,9 +4308,7 @@ export class CalendarViewElement extends LitElement {
4204
4308
  const { row } = this.getDayVisualPosition(dayIndex);
4205
4309
  const totalMinutes = hours * 60 + minutes;
4206
4310
  const rowY = week.yOffset + row * this.dayHeight;
4207
- return (
4208
- rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop
4209
- );
4311
+ return rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop;
4210
4312
  };
4211
4313
 
4212
4314
  const getDayColumnX = (day: Date) => {
@@ -4381,7 +4483,7 @@ export class CalendarViewElement extends LitElement {
4381
4483
  for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
4382
4484
  const day = week.days[dayIndex];
4383
4485
  if (!day) continue;
4384
-
4486
+
4385
4487
  const { row, col } = this.getDayVisualPosition(dayIndex);
4386
4488
  const x = col * dayWidth;
4387
4489
  const dayTop = week.yOffset + row * this.dayHeight - scrollTop;
@@ -4699,7 +4801,10 @@ export class CalendarViewElement extends LitElement {
4699
4801
 
4700
4802
  const col = Math.max(
4701
4803
  0,
4702
- Math.min(this._columnsPerRow - 1, Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth)),
4804
+ Math.min(
4805
+ this._columnsPerRow - 1,
4806
+ Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth),
4807
+ ),
4703
4808
  );
4704
4809
 
4705
4810
  // Find week at Y position
@@ -4712,9 +4817,9 @@ export class CalendarViewElement extends LitElement {
4712
4817
  // Calculate which visual row within the week
4713
4818
  const rowInWeek = Math.floor((y - week.yOffset) / this.dayHeight);
4714
4819
  const dayIndex = rowInWeek * this._columnsPerRow + col;
4715
-
4820
+
4716
4821
  if (dayIndex < 0 || dayIndex > 6) return null;
4717
-
4822
+
4718
4823
  const day = week.days[dayIndex];
4719
4824
  if (!day) return null;
4720
4825
 
@@ -4857,7 +4962,8 @@ export class CalendarViewElement extends LitElement {
4857
4962
  return html`<div class="notification-empty-state">No notifications set</div>`;
4858
4963
  }
4859
4964
 
4860
- return event.reminders.map(notif => html`
4965
+ return event.reminders.map(
4966
+ (notif) => html`
4861
4967
  <div class="notification-item">
4862
4968
  <select
4863
4969
  class="notification-select"
@@ -4867,14 +4973,20 @@ export class CalendarViewElement extends LitElement {
4867
4973
  this.updateNotification(event, notif.id, { triggerOffset: offset });
4868
4974
  }}
4869
4975
  >
4870
- ${NOTIFICATION_PRESETS.map(p => html`<option value="${p.value}" ?selected=${p.value === notif.triggerOffset}>${p.label}</option>`)}
4976
+ ${NOTIFICATION_PRESETS.map(
4977
+ (p) =>
4978
+ html`<option value="${p.value}" ?selected=${
4979
+ p.value === notif.triggerOffset
4980
+ }>${p.label}</option>`,
4981
+ )}
4871
4982
  </select>
4872
4983
  <button
4873
4984
  class="notification-remove-button"
4874
4985
  @click=${() => this.removeNotification(event, notif.id)}
4875
4986
  >×</button>
4876
4987
  </div>
4877
- `);
4988
+ `,
4989
+ );
4878
4990
  }
4879
4991
 
4880
4992
  addNotification(event: CalendarEvent) {
@@ -4884,31 +4996,41 @@ export class CalendarViewElement extends LitElement {
4884
4996
  enabled: true,
4885
4997
  };
4886
4998
  const reminders = [...(event.reminders || []), newNotif];
4887
- this.dispatchEvent(new CustomEvent("update-event", {
4888
- detail: { event, updates: { reminders } },
4889
- bubbles: true,
4890
- composed: true,
4891
- }));
4999
+ this.dispatchEvent(
5000
+ new CustomEvent("update-event", {
5001
+ detail: { event, updates: { reminders } },
5002
+ bubbles: true,
5003
+ composed: true,
5004
+ }),
5005
+ );
4892
5006
  }
4893
5007
 
4894
- updateNotification(event: CalendarEvent, notifId: string, updates: { triggerOffset: number }) {
4895
- const reminders = (event.reminders || []).map(n =>
4896
- n.id === notifId ? { ...n, ...updates } : n
5008
+ updateNotification(
5009
+ event: CalendarEvent,
5010
+ notifId: string,
5011
+ updates: { triggerOffset: number },
5012
+ ) {
5013
+ const reminders = (event.reminders || []).map((n) =>
5014
+ n.id === notifId ? { ...n, ...updates } : n,
5015
+ );
5016
+ this.dispatchEvent(
5017
+ new CustomEvent("update-event", {
5018
+ detail: { event, updates: { reminders } },
5019
+ bubbles: true,
5020
+ composed: true,
5021
+ }),
4897
5022
  );
4898
- this.dispatchEvent(new CustomEvent("update-event", {
4899
- detail: { event, updates: { reminders } },
4900
- bubbles: true,
4901
- composed: true,
4902
- }));
4903
5023
  }
4904
5024
 
4905
5025
  removeNotification(event: CalendarEvent, notifId: string) {
4906
- const reminders = (event.reminders || []).filter(n => n.id !== notifId);
4907
- this.dispatchEvent(new CustomEvent("update-event", {
4908
- detail: { event, updates: { reminders } },
4909
- bubbles: true,
4910
- composed: true,
4911
- }));
5026
+ const reminders = (event.reminders || []).filter((n) => n.id !== notifId);
5027
+ this.dispatchEvent(
5028
+ new CustomEvent("update-event", {
5029
+ detail: { event, updates: { reminders } },
5030
+ bubbles: true,
5031
+ composed: true,
5032
+ }),
5033
+ );
4912
5034
  }
4913
5035
 
4914
5036
  shouldRenderEventWithStripes(event: CalendarEvent): boolean {
@@ -4997,9 +5119,13 @@ export class CalendarViewElement extends LitElement {
4997
5119
  const containerHeight = this.scrollContainer?.clientHeight || 600;
4998
5120
 
4999
5121
  // Try positioning to the right, fallback to left if no space
5000
- const rightPosition = this.selectedEventRect.x + this.selectedEventRect.width + GAP;
5122
+ const rightPosition =
5123
+ this.selectedEventRect.x + this.selectedEventRect.width + GAP;
5001
5124
  const leftPosition = this.selectedEventRect.x - OVERLAY_WIDTH - GAP;
5002
- const preferredLeft = rightPosition + OVERLAY_WIDTH <= containerWidth ? rightPosition : leftPosition;
5125
+ const preferredLeft =
5126
+ rightPosition + OVERLAY_WIDTH <= containerWidth
5127
+ ? rightPosition
5128
+ : leftPosition;
5003
5129
 
5004
5130
  // Clamp to viewport boundaries
5005
5131
  const minLeft = LEFT_GUTTER_WIDTH + GAP;
@@ -5017,24 +5143,22 @@ export class CalendarViewElement extends LitElement {
5017
5143
 
5018
5144
  return html`
5019
5145
  <div class="event-detail-overlay" style="${style}">
5020
- <div class="event-detail-color-bar" style="opacity: ${
5021
- event.readOnly ? "0.5" : "1"
5022
- }"></div>
5023
-
5024
5146
  <div class="event-detail-content">
5025
5147
  <div class="event-detail-header">
5026
5148
  <div class="event-detail-header-content">
5027
5149
  ${
5028
- event.calendar
5029
- ? html`
5150
+ event.calendar
5151
+ ? html`
5030
5152
  <div class="event-detail-section event-detail-calendar">
5031
5153
  <div class="event-detail-value">${event.calendar}</div>
5032
5154
  </div>
5033
5155
  `
5034
- : null
5035
- }
5156
+ : null
5157
+ }
5036
5158
  ${
5037
- (event.readOnly || (event.organizer != null && !this.currentUserEmails.has(event.organizer.email)))
5159
+ event.readOnly ||
5160
+ (event.organizer != null &&
5161
+ !this.currentUserEmails.has(event.organizer.email))
5038
5162
  ? html`
5039
5163
  <h3 class="event-detail-title">${
5040
5164
  event.rrule ? html`<span style="opacity: 0.6">⟳</span> ` : ""
@@ -5192,12 +5316,15 @@ export class CalendarViewElement extends LitElement {
5192
5316
  : null;
5193
5317
  })()}
5194
5318
 
5195
- ${!event.readOnly ? html`
5319
+ ${
5320
+ !event.readOnly
5321
+ ? html`
5196
5322
  <div class="event-detail-section">
5197
5323
  <div class="event-detail-label">
5198
5324
  <span>Notifications</span>
5199
5325
 
5200
- <button class="notification-add-button" title="Add notification" @click=${() => this.addNotification(event)}>
5326
+ <button class="notification-add-button" title="Add notification" @click=${() =>
5327
+ this.addNotification(event)}>
5201
5328
  +
5202
5329
  </button>
5203
5330
  </div>
@@ -5205,10 +5332,14 @@ export class CalendarViewElement extends LitElement {
5205
5332
  ${this.renderNotificationsList(event)}
5206
5333
  </div>
5207
5334
  </div>
5208
- ` : null}
5335
+ `
5336
+ : null
5337
+ }
5209
5338
 
5210
5339
  ${
5211
- (event.readOnly || (event.organizer != null && !this.currentUserEmails.has(event.organizer.email)))
5340
+ event.readOnly ||
5341
+ (event.organizer != null &&
5342
+ !this.currentUserEmails.has(event.organizer.email))
5212
5343
  ? event.description
5213
5344
  ? html`
5214
5345
  <div class="event-detail-section">
@@ -5258,7 +5389,10 @@ export class CalendarViewElement extends LitElement {
5258
5389
  this.updateEventTimeout = setTimeout(() => {
5259
5390
  this.dispatchEvent(
5260
5391
  new CustomEvent("update-event", {
5261
- detail: { event, updates: { description: newDescription } },
5392
+ detail: {
5393
+ event,
5394
+ updates: { description: newDescription },
5395
+ },
5262
5396
  bubbles: true,
5263
5397
  composed: true,
5264
5398
  }),
@@ -5276,7 +5410,10 @@ export class CalendarViewElement extends LitElement {
5276
5410
  if (newDescription !== (event.description ?? "")) {
5277
5411
  this.dispatchEvent(
5278
5412
  new CustomEvent("update-event", {
5279
- detail: { event, updates: { description: newDescription } },
5413
+ detail: {
5414
+ event,
5415
+ updates: { description: newDescription },
5416
+ },
5280
5417
  bubbles: true,
5281
5418
  composed: true,
5282
5419
  }),
@@ -5352,6 +5489,11 @@ export class CalendarViewElement extends LitElement {
5352
5489
  <div class="toolbar-left">
5353
5490
  <slot name="toolbar-center"></slot>
5354
5491
 
5492
+ <button class="toolbar-button" title="Month" @click=${
5493
+ this.scrollToMonth
5494
+ }>
5495
+ Month
5496
+ </button>
5355
5497
  <button class="toolbar-button" title="Today" @click=${
5356
5498
  this.scrollToToday
5357
5499
  }>
@@ -5460,31 +5602,45 @@ export class CalendarViewElement extends LitElement {
5460
5602
  };
5461
5603
 
5462
5604
  return html`
5463
- <div class="notification-popover-overlay" @click=${this.onNotificationPopoverToggle}></div>
5605
+ <div class="notification-popover-overlay" @click=${
5606
+ this.onNotificationPopoverToggle
5607
+ }></div>
5464
5608
  <div class="notification-popover">
5465
5609
  <div class="notification-popover-header">
5466
5610
  <h3>Upcoming Notifications</h3>
5467
- <button class="notification-popover-close" @click=${this.onNotificationPopoverToggle}>×</button>
5611
+ <button class="notification-popover-close" @click=${
5612
+ this.onNotificationPopoverToggle
5613
+ }>×</button>
5468
5614
  </div>
5469
5615
  <div class="notification-popover-content">
5470
- ${this.scheduledNotifications.length === 0
5471
- ? html`<div class="notification-popover-empty">No scheduled notifications</div>`
5472
- : this.scheduledNotifications.map(
5473
- (notif) => html`
5616
+ ${
5617
+ this.scheduledNotifications.length === 0
5618
+ ? html`<div class="notification-popover-empty">No scheduled notifications</div>`
5619
+ : this.scheduledNotifications.map(
5620
+ (notif) => html`
5474
5621
  <div class="notification-popover-item">
5475
5622
  <div class="notification-popover-item-header">
5476
- <div class="notification-popover-item-title">${notif.eventTitle}</div>
5477
- <div class="notification-popover-item-time">${formatTriggerTime(notif.triggerTime)}</div>
5623
+ <div class="notification-popover-item-title">${
5624
+ notif.eventTitle
5625
+ }</div>
5626
+ <div class="notification-popover-item-time">${formatTriggerTime(
5627
+ notif.triggerTime,
5628
+ )}</div>
5478
5629
  </div>
5479
5630
  <div class="notification-popover-item-details">
5480
- <div class="notification-popover-item-event-time">📅 ${formatEventTime(notif.eventStart)}</div>
5481
- ${notif.eventLocation
5482
- ? html`<div class="notification-popover-item-location">📍 ${notif.eventLocation}</div>`
5483
- : null}
5631
+ <div class="notification-popover-item-event-time">📅 ${formatEventTime(
5632
+ notif.eventStart,
5633
+ )}</div>
5634
+ ${
5635
+ notif.eventLocation
5636
+ ? html`<div class="notification-popover-item-location">📍 ${notif.eventLocation}</div>`
5637
+ : null
5638
+ }
5484
5639
  </div>
5485
5640
  </div>
5486
5641
  `,
5487
- )}
5642
+ )
5643
+ }
5488
5644
  </div>
5489
5645
  </div>
5490
5646
  `;