@luckydye/calendar 1.1.2 → 1.2.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.
@@ -54,8 +54,7 @@ export class CalendarViewElement extends LitElement {
54
54
  display: var(--toolbar-display, flex);
55
55
  align-items: center;
56
56
  justify-content: space-between;
57
- height: 48px;
58
- padding: 0 16px;
57
+ padding: 8px 10px;
59
58
  background: var(--bg-secondary, rgba(36, 36, 38, 0.5));
60
59
  border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
61
60
  flex-shrink: 0;
@@ -100,7 +99,7 @@ export class CalendarViewElement extends LitElement {
100
99
  background: transparent;
101
100
  border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
102
101
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
103
- padding: 6px 12px;
102
+ padding: 4px 8px;
104
103
  border-radius: var(--border-radius-sm, 4px);
105
104
  font-size: 13px;
106
105
  line-height: 16px;
@@ -142,7 +141,7 @@ export class CalendarViewElement extends LitElement {
142
141
  background: var(--bg-input, rgba(0, 0, 0, 0.3));
143
142
  border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
144
143
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
145
- padding: 6px 12px 6px 32px;
144
+ padding: 4px 8px 4px 32px;
146
145
  border-radius: var(--border-radius-sm, 4px);
147
146
  font-size: 13px;
148
147
  outline: none;
@@ -165,12 +164,6 @@ export class CalendarViewElement extends LitElement {
165
164
  gap: 8px;
166
165
  }
167
166
 
168
- .toolbar-zoom-label {
169
- color: var(--text-muted, rgba(255, 255, 255, 0.6));
170
- font-size: 13px;
171
- white-space: nowrap;
172
- }
173
-
174
167
  .toolbar-zoom-slider {
175
168
  width: 100px;
176
169
  height: 4px;
@@ -211,38 +204,6 @@ export class CalendarViewElement extends LitElement {
211
204
  background: var(--text-primary, rgba(255, 255, 255, 1));
212
205
  }
213
206
 
214
- .header {
215
- display: flex;
216
- height: 32px;
217
- flex-shrink: 0;
218
- padding-right: 1rem;
219
- }
220
-
221
- .weekday:nth-child(7),
222
- .weekday:nth-child(6) {
223
- background: var(--bg-weekend, rgba(255, 255, 255, 0.03));
224
- }
225
-
226
- .header-gutter {
227
- width: 60px;
228
- flex-shrink: 0;
229
- }
230
-
231
- .weekdays {
232
- display: flex;
233
- flex: 1;
234
- }
235
-
236
- .weekday {
237
- flex: 1;
238
- display: flex;
239
- align-items: center;
240
- justify-content: center;
241
- font-size: 12px;
242
- color: var(--text-muted, rgba(255, 255, 255, 0.4));
243
- text-transform: uppercase;
244
- }
245
-
246
207
  .body {
247
208
  position: relative;
248
209
  flex: 1;
@@ -280,6 +241,10 @@ export class CalendarViewElement extends LitElement {
280
241
  overflow-anchor: none;
281
242
  }
282
243
 
244
+ :host([scroll-lock]) .scroll-container {
245
+ overflow: hidden;
246
+ }
247
+
283
248
  .scroll-container.zoom-cursor {
284
249
  cursor: ns-resize;
285
250
  }
@@ -369,21 +334,31 @@ export class CalendarViewElement extends LitElement {
369
334
  font-size: 16px;
370
335
  font-weight: 600;
371
336
  color: var(--text-primary, rgba(255, 255, 255, 0.9));
372
- background: var(--input-bg, rgba(0, 0, 0, 0.2));
373
337
  border: 1px solid var(--input-border, rgba(255, 255, 255, 0.1));
338
+ background: transparent;
374
339
  border-radius: 4px;
375
340
  padding: 6px 8px;
376
341
  width: 100%;
377
- outline: none;
378
342
  font-family: inherit;
379
343
  margin: 0 -8px 0 -8px;
380
344
  field-sizing: content;
381
345
  resize: none;
382
346
  }
383
347
 
348
+ .event-detail-description-input {
349
+ font-size: 14px;
350
+ color: inherit;
351
+ width: 100%;
352
+ padding: 0.25rem;
353
+ margin: -0.25rem;
354
+ border: none;
355
+ background: none;
356
+ field-sizing: content;
357
+ resize: none;
358
+ }
359
+
384
360
  .event-detail-title-input:focus {
385
361
  border-color: var(--input-border-focus, rgba(255, 255, 255, 0.3));
386
- background: var(--input-bg-focus, rgba(0, 0, 0, 0.3));
387
362
  }
388
363
 
389
364
  .event-detail-time {
@@ -404,6 +379,9 @@ export class CalendarViewElement extends LitElement {
404
379
  font-size: 20px;
405
380
  line-height: 1;
406
381
  flex-shrink: 0;
382
+ position: absolute;
383
+ top: 0.75rem;
384
+ right: 1rem;
407
385
  }
408
386
 
409
387
  .event-detail-close:hover {
@@ -430,6 +408,9 @@ export class CalendarViewElement extends LitElement {
430
408
  text-transform: uppercase;
431
409
  color: var(--text-muted, rgba(255, 255, 255, 0.4));
432
410
  letter-spacing: 0.5px;
411
+ display: flex;
412
+ align-items: center;
413
+ justify-content: space-between;
433
414
  }
434
415
 
435
416
  .event-detail-value {
@@ -453,6 +434,11 @@ export class CalendarViewElement extends LitElement {
453
434
  .event-detail-link {
454
435
  color: var(--accent-primary, rgb(100, 150, 255));
455
436
  text-decoration: none;
437
+ white-space: nowrap;
438
+ overflow: hidden;
439
+ text-overflow: ellipsis;
440
+ width: 100%;
441
+ display: block;
456
442
  }
457
443
 
458
444
  .event-detail-link:hover {
@@ -535,10 +521,10 @@ export class CalendarViewElement extends LitElement {
535
521
  border: 1px solid var(--grid-color);
536
522
  border-radius: 4px;
537
523
  color: var(--text-primary);
538
- padding: 6px 12px;
524
+ padding: 4px 8px;
525
+ margin: -4px 0;
539
526
  font-size: 13px;
540
527
  cursor: pointer;
541
- width: 100%;
542
528
  }
543
529
 
544
530
  .notification-add-button:hover {
@@ -764,19 +750,91 @@ export class CalendarViewElement extends LitElement {
764
750
  return this._scrollTop;
765
751
  }
766
752
 
767
- setView(dayHeight: number, scrollTop: number, syncScrollDOM = true): void {
768
- this._dayHeight = dayHeight;
769
- 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;
770
827
 
771
828
  this.saveDayHeight();
772
829
  this.updateWeekOffsets();
773
830
 
774
831
  if (syncScrollDOM && this.scrollContainer && this.scrollContent) {
775
- if (this.scrollContainer.scrollHeight < scrollTop) {
776
- this.scrollContent.style.minHeight =
777
- scrollTop + window.innerHeight + "px";
832
+ if (this.scrollContainer.scrollHeight < toScrollTop) {
833
+ this.scrollContent.style.minHeight = `${
834
+ toScrollTop + window.innerHeight
835
+ }px`;
778
836
  }
779
- this.scrollContainer.scrollTop = scrollTop;
837
+ this.scrollContainer.scrollTop = toScrollTop;
780
838
  }
781
839
 
782
840
  this.saveScrollPosition();
@@ -846,6 +904,55 @@ export class CalendarViewElement extends LitElement {
846
904
  resizingOriginalEnd: Date | null = null;
847
905
  isResizingEvent = false;
848
906
 
907
+ _columnsPerRow = 7;
908
+ set columnsPerRow(value: number) {
909
+ const clamped = Math.max(1, Math.min(7, Math.floor(value)));
910
+ if (this._columnsPerRow !== clamped) {
911
+ this._columnsPerRow = clamped;
912
+ this.updateWeekOffsets();
913
+ this.renderCanvas();
914
+ this.requestUpdate();
915
+ }
916
+ }
917
+ get columnsPerRow(): number {
918
+ return this._columnsPerRow;
919
+ }
920
+
921
+ get rowsPerWeek(): number {
922
+ return Math.ceil(7 / this._columnsPerRow);
923
+ }
924
+
925
+ getDayVisualPosition(dayIndex: number): { row: number; col: number } {
926
+ const row = Math.floor(dayIndex / this._columnsPerRow);
927
+ const col = dayIndex % this._columnsPerRow;
928
+ return { row, col };
929
+ }
930
+
931
+ getVisualPositionFromCoords(
932
+ x: number,
933
+ y: number,
934
+ gridWidth: number,
935
+ ): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
936
+ if (!this.scrollContainer) return null;
937
+
938
+ const dayWidth = gridWidth / this._columnsPerRow;
939
+ const col = Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth);
940
+ if (col < 0 || col >= this._columnsPerRow) return null;
941
+
942
+ const week = this.weeks.find(
943
+ (w) => w.height > 0 && y >= w.yOffset && y < w.yOffset + w.height,
944
+ );
945
+ if (!week) return null;
946
+
947
+ const rowHeight = this.dayHeight;
948
+ const row = Math.floor((y - week.yOffset) / rowHeight);
949
+ const dayIndex = row * this._columnsPerRow + col;
950
+ if (dayIndex < 0 || dayIndex > 6) return null;
951
+
952
+ const timeFraction = ((y - week.yOffset) % rowHeight) / rowHeight;
953
+ return { dayIndex, timeFraction, weekYOffset: week.yOffset };
954
+ }
955
+
849
956
  // History stack for scroll position and zoom
850
957
  // Index 0 is always the most recent entry
851
958
  historyStack: Array<{ scrollTop: number; dayHeight: number }> = [];
@@ -905,8 +1012,15 @@ export class CalendarViewElement extends LitElement {
905
1012
  this.internal = new CalendarInternal({
906
1013
  locale: this.getAttribute("locale") || undefined,
907
1014
  weekStart: Number(this.getAttribute("week-start")),
908
- storage: new IndexedDBStorage()
1015
+ storage: new IndexedDBStorage(),
909
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);
910
1024
  }
911
1025
 
912
1026
  loadDayHeight(): number {
@@ -956,8 +1070,43 @@ export class CalendarViewElement extends LitElement {
956
1070
  }
957
1071
  }
958
1072
 
959
- scrollToToday = (): void => {
960
- this.scrollToDate(new Date(), 0.5, true, true);
1073
+ scrollToToday = (animate = true): void => {
1074
+ const now = new Date();
1075
+
1076
+ let weekIndex = this.weeks.findIndex((w) =>
1077
+ w.days.some((d) => CalendarInternal.isSameDay(d, now)),
1078
+ );
1079
+ if (weekIndex < 0) {
1080
+ this.weeks = this.internal.resetRangeAroundDate(now);
1081
+ weekIndex = this.weeks.findIndex((w) =>
1082
+ w.days.some((d) => CalendarInternal.isSameDay(d, now)),
1083
+ );
1084
+ }
1085
+
1086
+ let offsetInWeek = 0.5;
1087
+ if (weekIndex >= 0) {
1088
+ const targetWeek = this.weeks[weekIndex];
1089
+ const todayIndex = targetWeek.days.findIndex((d) =>
1090
+ CalendarInternal.isSameDay(d, now),
1091
+ );
1092
+ if (todayIndex >= 0) {
1093
+ const row = Math.floor(todayIndex / this._columnsPerRow);
1094
+ const timeFraction = (now.getHours() + now.getMinutes() / 60) / 24;
1095
+ offsetInWeek = (row + timeFraction) / this.rowsPerWeek;
1096
+ }
1097
+ }
1098
+
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);
961
1110
  };
962
1111
 
963
1112
  // History management methods
@@ -1045,7 +1194,10 @@ export class CalendarViewElement extends LitElement {
1045
1194
  offsetInWeek = 0.5,
1046
1195
  animate = false,
1047
1196
  extendRange = false,
1197
+ targetDayHeight?: number,
1048
1198
  ): void {
1199
+ const dayHeight = targetDayHeight ?? this._dayHeight;
1200
+
1049
1201
  let weekIndex = this.weeks.findIndex(
1050
1202
  (w) =>
1051
1203
  w.days.some((d) => CalendarInternal.isSameDay(d, targetDate)) ||
@@ -1058,7 +1210,6 @@ export class CalendarViewElement extends LitElement {
1058
1210
  // If date not found in current buffer, reset range around target (only if explicitly requested)
1059
1211
  if (weekIndex < 0 && extendRange) {
1060
1212
  this.weeks = this.internal.resetRangeAroundDate(targetDate);
1061
- this.updateWeekOffsets();
1062
1213
 
1063
1214
  // Find the week again in the new range
1064
1215
  weekIndex = this.weeks.findIndex(
@@ -1072,15 +1223,24 @@ export class CalendarViewElement extends LitElement {
1072
1223
  }
1073
1224
 
1074
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
+
1075
1231
  const targetWeek = this.weeks[weekIndex];
1076
1232
  if (targetWeek) {
1077
1233
  const targetY = targetWeek.yOffset + targetWeek.height * offsetInWeek;
1078
1234
  const targetScroll = Math.max(0, targetY - this.viewportHeight / 2);
1079
- if (animate) {
1080
- this.animateScrollTo(targetScroll);
1081
- } else {
1082
- this.scrollTop = targetScroll;
1083
- }
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();
1084
1244
  }
1085
1245
  }
1086
1246
  }
@@ -1092,39 +1252,6 @@ export class CalendarViewElement extends LitElement {
1092
1252
  }
1093
1253
  };
1094
1254
 
1095
- private animateScrollTo(targetScroll: number): void {
1096
- this.cancelScrollAnimation();
1097
-
1098
- if (!this.scrollContainer) return;
1099
-
1100
- const startScroll = this.scrollTop;
1101
- const distance = targetScroll - startScroll;
1102
- const duration = 800;
1103
- const startTime = performance.now();
1104
-
1105
- const easeOutExpo = (t: number): number => {
1106
- return t === 1 ? 1 : 1 - 2 ** (-10 * t);
1107
- };
1108
-
1109
- const animate = (currentTime: number): void => {
1110
- if (!this.scrollAnimationFrame) return;
1111
-
1112
- const elapsed = currentTime - startTime;
1113
- const progress = Math.min(elapsed / duration, 1);
1114
- const easedProgress = easeOutExpo(progress);
1115
-
1116
- this.scrollTop = startScroll + distance * easedProgress;
1117
-
1118
- if (progress < 1) {
1119
- this.scrollAnimationFrame = requestAnimationFrame(animate);
1120
- } else {
1121
- this.scrollAnimationFrame = null;
1122
- }
1123
- };
1124
-
1125
- this.scrollAnimationFrame = requestAnimationFrame(animate);
1126
- }
1127
-
1128
1255
  // Push current state when entering filter mode
1129
1256
  private pushFilterToHistory(): void {
1130
1257
  if (this.saveHistoryTimeout) {
@@ -1179,27 +1306,16 @@ export class CalendarViewElement extends LitElement {
1179
1306
 
1180
1307
  window.addEventListener("mousemove", this.onMouseMove);
1181
1308
  window.addEventListener("mouseup", this.onMouseUp);
1182
- window.addEventListener("wheel", this.onWheel, { passive: false });
1183
- window.addEventListener("keydown", this.onKeyDown);
1184
1309
  window.addEventListener("paste", this.onPaste);
1185
- this.addEventListener("dragstart", this.onDragStart);
1186
- this.addEventListener("dragend", this.onDragEnd);
1187
- this.addEventListener("dragover", this.onDragOver);
1188
- this.addEventListener("dragleave", this.onDragLeave);
1189
- this.addEventListener("drop", this.onDrop);
1190
1310
  }
1191
1311
 
1192
1312
  disconnectedCallback() {
1193
1313
  super.disconnectedCallback();
1194
- document.removeEventListener("mousemove", this.onMouseMove);
1195
- document.removeEventListener("mouseup", this.onMouseUp);
1196
- window.removeEventListener("keydown", this.onKeyDown);
1314
+
1315
+ window.removeEventListener("mousemove", this.onMouseMove);
1316
+ window.removeEventListener("mouseup", this.onMouseUp);
1197
1317
  window.removeEventListener("paste", this.onPaste);
1198
- this.removeEventListener("dragstart", this.onDragStart);
1199
- this.removeEventListener("dragend", this.onDragEnd);
1200
- this.removeEventListener("dragover", this.onDragOver);
1201
- this.removeEventListener("dragleave", this.onDragLeave);
1202
- this.removeEventListener("drop", this.onDrop);
1318
+
1203
1319
  if (this.scrollContainer) {
1204
1320
  this.scrollContainer.removeEventListener(
1205
1321
  "mouseleave",
@@ -1224,6 +1340,25 @@ export class CalendarViewElement extends LitElement {
1224
1340
 
1225
1341
  updated() {
1226
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`;
1227
1362
  }
1228
1363
 
1229
1364
  async firstUpdated() {
@@ -1246,7 +1381,9 @@ export class CalendarViewElement extends LitElement {
1246
1381
  }
1247
1382
 
1248
1383
  if (this.scrollContainer) {
1249
- this.scrollContainer.addEventListener("scroll", this.onScroll);
1384
+ this.scrollContainer.addEventListener("scroll", this.onScroll, {
1385
+ passive: false,
1386
+ });
1250
1387
  this.scrollContainer.addEventListener(
1251
1388
  "mouseleave",
1252
1389
  this.onScrollContainerMouseLeave,
@@ -1323,6 +1460,7 @@ export class CalendarViewElement extends LitElement {
1323
1460
 
1324
1461
  updateWeekOffsets(): void {
1325
1462
  let y = 0;
1463
+ const weekHeight = this.dayHeight * this.rowsPerWeek;
1326
1464
 
1327
1465
  if (this.filter) {
1328
1466
  const filteredEvents = this.events;
@@ -1345,13 +1483,13 @@ export class CalendarViewElement extends LitElement {
1345
1483
  (range) => range.end >= weekStartTime && range.start <= weekEndTime,
1346
1484
  );
1347
1485
 
1348
- week.height = hasEvents ? this.dayHeight : 0;
1486
+ week.height = hasEvents ? weekHeight : 0;
1349
1487
  y += week.height;
1350
1488
  }
1351
1489
  } else {
1352
1490
  for (const week of this.weeks) {
1353
1491
  week.yOffset = y;
1354
- week.height = this.dayHeight;
1492
+ week.height = weekHeight;
1355
1493
  y += week.height;
1356
1494
  }
1357
1495
  }
@@ -1391,21 +1529,43 @@ export class CalendarViewElement extends LitElement {
1391
1529
  }
1392
1530
  }
1393
1531
 
1532
+ this.updateColumnsForViewport();
1394
1533
  this.renderCanvas();
1395
1534
  }
1396
1535
 
1536
+ updateColumnsForViewport(): void {
1537
+ if (!this.scrollContainer) return;
1538
+
1539
+ const rect = this.scrollContainer.getBoundingClientRect();
1540
+ const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
1541
+
1542
+ // Determine optimal columns based on available width
1543
+ // Minimum 60px per day column for usability
1544
+ const minDayWidth = 120;
1545
+ const optimalColumns = Math.max(
1546
+ 1,
1547
+ Math.min(7, Math.floor(gridWidth / minDayWidth)),
1548
+ );
1549
+
1550
+ if (this._columnsPerRow !== optimalColumns) {
1551
+ this._columnsPerRow = optimalColumns;
1552
+ this.updateWeekOffsets();
1553
+ }
1554
+ }
1555
+
1397
1556
  renderCanvas(): void {
1398
1557
  if (!this.ctx || !this.canvas || !this.scrollContainer) return;
1399
1558
 
1400
1559
  const ctx = this.ctx;
1401
1560
  const width = this.canvas.width / (window.devicePixelRatio || 1);
1402
1561
  const height = this.canvas.height / (window.devicePixelRatio || 1);
1562
+ const fontFamily = getComputedStyle(this).fontFamily;
1403
1563
 
1404
1564
  ctx.clearRect(0, 0, width, height);
1405
1565
 
1406
1566
  const scrollTop = this.scrollTop;
1407
1567
  const gridWidth = width - LEFT_GUTTER_WIDTH;
1408
- const dayWidth = gridWidth / 7;
1568
+ const dayWidth = gridWidth / this._columnsPerRow;
1409
1569
  const today = new Date();
1410
1570
 
1411
1571
  // Find visible weeks
@@ -1423,19 +1583,20 @@ export class CalendarViewElement extends LitElement {
1423
1583
  CalendarInternal.isSameDay(d, today),
1424
1584
  );
1425
1585
  if (todayIndex >= 0) {
1426
- const x = LEFT_GUTTER_WIDTH + todayIndex * dayWidth;
1427
- const y = week.yOffset - scrollTop;
1586
+ const { row, col } = this.getDayVisualPosition(todayIndex);
1587
+ const x = LEFT_GUTTER_WIDTH + col * dayWidth;
1588
+ const dayY = week.yOffset + row * this.dayHeight - scrollTop;
1428
1589
  const bgToday =
1429
1590
  getComputedStyle(this).getPropertyValue("--bg-today").trim() ||
1430
1591
  "rgba(255, 255, 255, 0.05)";
1431
1592
  ctx.fillStyle = bgToday;
1432
- ctx.fillRect(x, y, dayWidth, week.height);
1593
+ ctx.fillRect(x, dayY, dayWidth, this.dayHeight);
1433
1594
 
1434
1595
  if (showTimeScale) {
1435
1596
  // Draw current time indicator line (zoomed in)
1436
1597
  const now = new Date();
1437
1598
  const currentMinutes = now.getHours() * 60 + now.getMinutes();
1438
- const timeY = y + (currentMinutes / 1440) * week.height;
1599
+ const timeY = dayY + (currentMinutes / 1440) * this.dayHeight;
1439
1600
  if (timeY >= 0 && timeY <= height) {
1440
1601
  const accentTime =
1441
1602
  getComputedStyle(this)
@@ -1459,8 +1620,8 @@ export class CalendarViewElement extends LitElement {
1459
1620
  "rgba(255, 255, 255, 0.1)";
1460
1621
  ctx.lineWidth = 1;
1461
1622
 
1462
- // Vertical lines (day separators)
1463
- for (let i = 1; i <= 7; i++) {
1623
+ // Vertical lines (day separators) - draw for each column
1624
+ for (let i = 1; i <= this._columnsPerRow; i++) {
1464
1625
  const x = LEFT_GUTTER_WIDTH + i * dayWidth;
1465
1626
  ctx.beginPath();
1466
1627
  ctx.moveTo(x, 0);
@@ -1541,15 +1702,28 @@ export class CalendarViewElement extends LitElement {
1541
1702
  ctx.lineTo(width, y);
1542
1703
  ctx.stroke();
1543
1704
 
1705
+ // Draw horizontal lines between visual rows within this week
1706
+ for (let row = 1; row < this.rowsPerWeek; row++) {
1707
+ const rowY = y + row * this.dayHeight;
1708
+ if (rowY >= 0 && rowY <= height) {
1709
+ ctx.beginPath();
1710
+ ctx.moveTo(LEFT_GUTTER_WIDTH, rowY);
1711
+ ctx.lineTo(width, rowY);
1712
+ ctx.stroke();
1713
+ }
1714
+ }
1715
+
1544
1716
  // Left gutter: week number and time scale
1545
1717
  const hourLabelOpacity = Math.max(
1546
1718
  0,
1547
1719
  Math.min(1, (this.dayHeight - 300) / 300),
1548
1720
  );
1549
1721
 
1722
+ ctx.font = `500 11px ${fontFamily}`;
1723
+ ctx.textBaseline = "bottom";
1550
1724
  ctx.textAlign = "right";
1551
1725
 
1552
- // Draw hourly lines and labels
1726
+ // Draw hourly lines and labels for each visual row
1553
1727
  const gridColor =
1554
1728
  getComputedStyle(this).getPropertyValue("--grid-color").trim() ||
1555
1729
  "rgba(255, 255, 255, 0.1)";
@@ -1568,19 +1742,22 @@ export class CalendarViewElement extends LitElement {
1568
1742
  `${0.4 * hourLabelOpacity})`,
1569
1743
  );
1570
1744
 
1571
- for (let hour = 0; hour < 24; hour++) {
1572
- const hourY = y + (hour / 24) * week.height;
1573
- if (hourY >= 0 && hourY <= height) {
1574
- // Hour line
1575
- ctx.beginPath();
1576
- ctx.moveTo(LEFT_GUTTER_WIDTH, hourY);
1577
- ctx.lineTo(width, hourY);
1578
- ctx.stroke();
1745
+ for (let row = 0; row < this.rowsPerWeek; row++) {
1746
+ const rowY = y + row * this.dayHeight;
1747
+ for (let hour = 0; hour < 24; hour++) {
1748
+ const hourY = rowY + (hour / 24) * this.dayHeight;
1749
+ if (hourY >= 0 && hourY <= height) {
1750
+ // Hour line
1751
+ ctx.beginPath();
1752
+ ctx.moveTo(LEFT_GUTTER_WIDTH, hourY);
1753
+ ctx.lineTo(width, hourY);
1754
+ ctx.stroke();
1579
1755
 
1580
- // Hour label (only draw if opacity is significant)
1581
- if (hourLabelOpacity > 0.1) {
1582
- const label = `${hour.toString().padStart(2, "0")}:00`;
1583
- ctx.fillText(label, 48, hourY + 4);
1756
+ // Hour label (only draw if opacity is significant)
1757
+ if (hourLabelOpacity > 0.1) {
1758
+ const label = `${hour.toString().padStart(2, "0")}:00`;
1759
+ ctx.fillText(label, 48, hourY + 4);
1760
+ }
1584
1761
  }
1585
1762
  }
1586
1763
  }
@@ -1588,13 +1765,15 @@ export class CalendarViewElement extends LitElement {
1588
1765
  // Draw current time indicator in left gutter (only when timescale is visible and today is in this week)
1589
1766
  if (hourLabelOpacity > 0.1) {
1590
1767
  const today = new Date();
1591
- const isTodayInWeek = week.days.some((d) =>
1768
+ const todayIndex = week.days.findIndex((d) =>
1592
1769
  CalendarInternal.isSameDay(d, today),
1593
1770
  );
1594
1771
 
1595
- if (isTodayInWeek) {
1772
+ if (todayIndex >= 0) {
1773
+ const { row } = this.getDayVisualPosition(todayIndex);
1596
1774
  const currentMinutes = today.getHours() * 60 + today.getMinutes();
1597
- const timeY = y + (currentMinutes / 1440) * week.height;
1775
+ const timeY =
1776
+ y + row * this.dayHeight + (currentMinutes / 1440) * this.dayHeight;
1598
1777
 
1599
1778
  if (timeY >= 0 && timeY <= height) {
1600
1779
  const hours = today.getHours().toString().padStart(2, "0");
@@ -1627,7 +1806,10 @@ export class CalendarViewElement extends LitElement {
1627
1806
  ctx.fill();
1628
1807
 
1629
1808
  // Draw current time text in white
1630
- ctx.fillStyle = "white";
1809
+ ctx.fillStyle =
1810
+ getComputedStyle(this)
1811
+ .getPropertyValue("--text-primary")
1812
+ .trim() || "rgba(255, 255, 255, 1)";
1631
1813
  ctx.fillText(timeText, textX, textY);
1632
1814
  ctx.restore();
1633
1815
  }
@@ -1671,6 +1853,10 @@ export class CalendarViewElement extends LitElement {
1671
1853
  this.renderEventsOnCanvas(ctx, scrollTop, height, dayWidth, visibleWeeks);
1672
1854
 
1673
1855
  this.renderDateLabels();
1856
+
1857
+ // Draw sticky weekday labels at top of viewport
1858
+ this.renderWeekdayLabels(ctx, dayWidth, visibleWeeks, scrollTop, height);
1859
+
1674
1860
  if (this.isCreatingEvent) {
1675
1861
  this.renderEventCreationPreview();
1676
1862
  }
@@ -1800,7 +1986,7 @@ export class CalendarViewElement extends LitElement {
1800
1986
  });
1801
1987
  }
1802
1988
  return result;
1803
- })
1989
+ })
1804
1990
  : timedSegments;
1805
1991
 
1806
1992
  // Combine segments with all-day events first (so they get top rows)
@@ -1946,11 +2132,21 @@ export class CalendarViewElement extends LitElement {
1946
2132
  isEnd,
1947
2133
  totalWeeks,
1948
2134
  } = segment;
1949
- const weekHeight = week.height;
1950
2135
  const weekYOffset = week.yOffset;
1951
2136
 
1952
2137
  const allDay = event.isAllDay === true;
1953
2138
 
2139
+ // Get visual position for the start day
2140
+ const startVisualPos = this.getDayVisualPosition(startDayIndex);
2141
+ const endVisualPos = this.getDayVisualPosition(endDayIndex);
2142
+
2143
+ // Skip segments that span multiple visual rows for now (would need to be split)
2144
+ // For simple case, render each day on its visual row
2145
+ if (startVisualPos.row !== endVisualPos.row) {
2146
+ // Event spans visual rows - for now, only render the start portion
2147
+ // TODO: Split into multiple segments across visual rows
2148
+ }
2149
+
1954
2150
  let yStart: number;
1955
2151
  let yEnd: number;
1956
2152
 
@@ -1981,8 +2177,10 @@ export class CalendarViewElement extends LitElement {
1981
2177
  const endMinutes =
1982
2178
  effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
1983
2179
 
1984
- yStart = weekYOffset + (startMinutes / 1440) * weekHeight;
1985
- yEnd = weekYOffset + (endMinutes / 1440) * weekHeight;
2180
+ // Position within the visual row
2181
+ const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
2182
+ yStart = visualRowY + (startMinutes / 1440) * this.dayHeight;
2183
+ yEnd = visualRowY + (endMinutes / 1440) * this.dayHeight;
1986
2184
  } else {
1987
2185
  const eventKey = `${weekIndex}-${event.id}`;
1988
2186
  let rowIndex = eventRowIndex.get(eventKey);
@@ -2015,12 +2213,14 @@ export class CalendarViewElement extends LitElement {
2015
2213
  occupied.add(rowIndex);
2016
2214
  }
2017
2215
 
2018
- const maxEventsInWeek = Math.floor(
2019
- (weekHeight - 4) / (MIN_EVENT_HEIGHT + 2),
2216
+ const maxEventsInRow = Math.floor(
2217
+ (this.dayHeight - 4) / (MIN_EVENT_HEIGHT + 2),
2020
2218
  );
2021
- if (rowIndex >= maxEventsInWeek) continue;
2219
+ if (rowIndex >= maxEventsInRow) continue;
2022
2220
 
2023
- yStart = weekYOffset + 4 + rowIndex * (MIN_EVENT_HEIGHT + 2);
2221
+ // Position within the visual row
2222
+ const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
2223
+ yStart = visualRowY + 4 + rowIndex * (MIN_EVENT_HEIGHT + 2);
2024
2224
  yEnd = yStart + MIN_EVENT_HEIGHT;
2025
2225
  }
2026
2226
 
@@ -2046,13 +2246,15 @@ export class CalendarViewElement extends LitElement {
2046
2246
  const columnWidth = dayWidth / columnLayout.totalColumns;
2047
2247
  x =
2048
2248
  LEFT_GUTTER_WIDTH +
2049
- startDayIndex * dayWidth +
2249
+ startVisualPos.col * dayWidth +
2050
2250
  columnLayout.column * columnWidth;
2051
2251
  spanWidth = columnWidth;
2052
2252
  } else {
2053
- // Normal layout
2054
- x = LEFT_GUTTER_WIDTH + startDayIndex * dayWidth;
2055
- spanWidth = (endDayIndex - startDayIndex + 1) * dayWidth;
2253
+ // Normal layout - use visual column position
2254
+ // For multi-day events on same visual row, calculate span
2255
+ const colSpan = endVisualPos.col - startVisualPos.col + 1;
2256
+ x = LEFT_GUTTER_WIDTH + startVisualPos.col * dayWidth;
2257
+ spanWidth = colSpan * dayWidth;
2056
2258
  }
2057
2259
 
2058
2260
  // Convert to viewport coordinates
@@ -2424,7 +2626,7 @@ export class CalendarViewElement extends LitElement {
2424
2626
  const stickyTop = Math.max(0, scrollTop - labelY);
2425
2627
  const maxStickyTop = nextMonthY - labelY - 24;
2426
2628
  const clampedStickyTop = Math.min(stickyTop, maxStickyTop);
2427
- const labelTopMargin = 0;
2629
+ const labelTopMargin = 32;
2428
2630
  const finalTop = labelY + clampedStickyTop - scrollTop + labelTopMargin;
2429
2631
 
2430
2632
  ctx.save();
@@ -2435,7 +2637,7 @@ export class CalendarViewElement extends LitElement {
2435
2637
  const labelText = `${month.monthName} ${month.year}`;
2436
2638
  const textWidth = ctx.measureText(labelText).width;
2437
2639
  const leftMargin = 8;
2438
- const textX = 0 + padding[3] + leftMargin;
2640
+ const textX = 64 + padding[3] + leftMargin;
2439
2641
  const textY = finalTop + padding[0];
2440
2642
 
2441
2643
  // Draw background
@@ -2466,6 +2668,8 @@ export class CalendarViewElement extends LitElement {
2466
2668
  }
2467
2669
 
2468
2670
  onWheel = (e: WheelEvent): void => {
2671
+ if (this.hasAttribute("scroll-lock")) return;
2672
+
2469
2673
  if (this.scrollAnimationFrame) {
2470
2674
  this.cancelScrollAnimation();
2471
2675
  }
@@ -2953,110 +3157,84 @@ export class CalendarViewElement extends LitElement {
2953
3157
  }
2954
3158
  };
2955
3159
 
2956
- onKeyDown = (e: KeyboardEvent): void => {
2957
- if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "r") {
2958
- e.preventDefault();
2959
- this.dispatchEvent(
2960
- new CustomEvent("force-sync", {
2961
- bubbles: true,
2962
- }),
2963
- );
2964
- } else if ((e.metaKey || e.ctrlKey) && e.key === "c") {
2965
- // Don't interfere with copying text from input fields
2966
- const target = e.target as HTMLElement;
2967
- if (target.matches('input, textarea, [contenteditable="true"]')) {
2968
- return;
2969
- }
2970
-
2971
- const selectedEvents = this.internal.getSelectedEvents();
2972
- if (selectedEvents.length === 0) return;
2973
-
2974
- e.preventDefault();
3160
+ forceSync(): void {
3161
+ this.dispatchEvent(new CustomEvent("force-sync", { bubbles: true }));
3162
+ }
2975
3163
 
2976
- const icalText = serializeEventsToICal(selectedEvents);
2977
- const blob = new Blob([icalText], { type: "text/plain" });
2978
- const item = new ClipboardItem({ "text/plain": blob });
2979
- navigator.clipboard.write([item]).then(() => {
2980
- const count = selectedEvents.length;
2981
- queueStatus(
2982
- `Copied ${count} event${count === 1 ? "" : "s"} to clipboard`,
2983
- );
2984
- });
2985
- } else if (e.key === "Backspace" || e.key === "Delete") {
2986
- // Don't delete events if user is typing in an input field
2987
- const target = e.target as HTMLElement;
2988
- if (target.matches('input, textarea, [contenteditable="true"]')) {
2989
- return;
2990
- }
3164
+ copySelectedEvents(): void {
3165
+ const selectedEvents = this.internal.getSelectedEvents();
3166
+ if (selectedEvents.length === 0) return;
3167
+
3168
+ const icalText = serializeEventsToICal(selectedEvents);
3169
+ const blob = new Blob([icalText], { type: "text/plain" });
3170
+ const item = new ClipboardItem({ "text/plain": blob });
3171
+ navigator.clipboard.write([item]).then(() => {
3172
+ const count = selectedEvents.length;
3173
+ queueStatus(
3174
+ `Copied ${count} event${count === 1 ? "" : "s"} to clipboard`,
3175
+ );
3176
+ });
3177
+ }
2991
3178
 
2992
- const selectedEvents = this.internal.getSelectedEvents();
2993
- if (selectedEvents.length === 0) return;
3179
+ deleteSelectedEvents(): void {
3180
+ const selectedEvents = this.internal.getSelectedEvents();
3181
+ if (selectedEvents.length === 0) return;
2994
3182
 
2995
- // Prevent deleting read-only events
2996
- const deletableEvents = selectedEvents.filter((event) => !event.readOnly);
2997
- if (deletableEvents.length === 0) return;
3183
+ const deletableEvents = selectedEvents.filter((event) => !event.readOnly);
3184
+ if (deletableEvents.length === 0) return;
2998
3185
 
2999
- // Prevent default behavior (like going back in browser)
3000
- e.preventDefault();
3186
+ this.dispatchEvent(
3187
+ new CustomEvent("delete-events", {
3188
+ detail: { events: deletableEvents },
3189
+ bubbles: true,
3190
+ }),
3191
+ );
3001
3192
 
3002
- this.dispatchEvent(
3003
- new CustomEvent("delete-events", {
3004
- detail: { events: deletableEvents },
3005
- bubbles: true,
3006
- }),
3007
- );
3193
+ this.internal.clearSelection();
3194
+ this.selectedEventForDetail = null;
3195
+ this.selectedEventRect = null;
3196
+ this.dispatchEvent(
3197
+ new CustomEvent("selection-change", {
3198
+ detail: { selectedEvents: [] },
3199
+ bubbles: true,
3200
+ }),
3201
+ );
3202
+ }
3008
3203
 
3009
- // Clear selection after delete
3204
+ escape(): void {
3205
+ if (this.isCreatingEvent || this.eventCreationStart) {
3206
+ this.clearEventCreationState();
3207
+ this.renderCanvas();
3208
+ queueStatus("Event creation cancelled");
3209
+ } else if (this.isDraggingEvent || this.movingEvent) {
3210
+ this.resetDragState();
3211
+ this.renderCanvas();
3212
+ queueStatus("Event move cancelled");
3213
+ } else if (this.isResizingEvent) {
3214
+ this.isResizingEvent = false;
3215
+ this.resizeEvent = null;
3216
+ this.resizeStartY = 0;
3217
+ this.resizeEdge = null;
3218
+ this.renderCanvas();
3219
+ queueStatus("Event resize cancelled");
3220
+ } else if (
3221
+ this.internal.getSelectedEvents().length > 0 ||
3222
+ this.selectedEventForDetail
3223
+ ) {
3010
3224
  this.internal.clearSelection();
3011
3225
  this.selectedEventForDetail = null;
3012
3226
  this.selectedEventRect = null;
3227
+ this.isDescriptionExpanded = false;
3228
+ this.renderCanvas();
3229
+ this.requestUpdate();
3013
3230
  this.dispatchEvent(
3014
3231
  new CustomEvent("selection-change", {
3015
3232
  detail: { selectedEvents: [] },
3016
3233
  bubbles: true,
3017
3234
  }),
3018
3235
  );
3019
- } else if (e.key === "Escape") {
3020
- // Cancel any active dragging/creating operations
3021
- if (this.isCreatingEvent || this.eventCreationStart) {
3022
- e.preventDefault();
3023
- this.clearEventCreationState();
3024
- this.renderCanvas();
3025
- queueStatus("Event creation cancelled");
3026
- } else if (this.isDraggingEvent || this.movingEvent) {
3027
- e.preventDefault();
3028
- this.resetDragState();
3029
- this.renderCanvas();
3030
- queueStatus("Event move cancelled");
3031
- } else if (this.isResizingEvent) {
3032
- e.preventDefault();
3033
- this.isResizingEvent = false;
3034
- this.resizeEvent = null;
3035
- this.resizeStartY = 0;
3036
- this.resizeEdge = null;
3037
- this.renderCanvas();
3038
- queueStatus("Event resize cancelled");
3039
- } else if (
3040
- this.internal.getSelectedEvents().length > 0 ||
3041
- this.selectedEventForDetail
3042
- ) {
3043
- // Clear selection if there are selected events or detail overlay is open
3044
- e.preventDefault();
3045
- this.internal.clearSelection();
3046
- this.selectedEventForDetail = null;
3047
- this.selectedEventRect = null;
3048
- this.isDescriptionExpanded = false;
3049
- this.renderCanvas();
3050
- this.requestUpdate();
3051
- this.dispatchEvent(
3052
- new CustomEvent("selection-change", {
3053
- detail: { selectedEvents: [] },
3054
- bubbles: true,
3055
- }),
3056
- );
3057
- }
3058
3236
  }
3059
- };
3237
+ }
3060
3238
 
3061
3239
  onScrollContainerMouseMove = (e: MouseEvent): void => {
3062
3240
  if (!this.scrollContainer || this.isDraggingZoom) return;
@@ -3506,16 +3684,22 @@ export class CalendarViewElement extends LitElement {
3506
3684
 
3507
3685
  const rect = this.scrollContainer.getBoundingClientRect();
3508
3686
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
3509
- const dayWidth = gridWidth / 7;
3687
+ const dayWidth = gridWidth / this._columnsPerRow;
3510
3688
 
3511
- // Find day indices from X coordinates
3512
- const startDayIndex = Math.max(
3689
+ // Find column indices from X coordinates
3690
+ const startCol = Math.max(
3513
3691
  0,
3514
- Math.min(6, Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth)),
3692
+ Math.min(
3693
+ this._columnsPerRow - 1,
3694
+ Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth),
3695
+ ),
3515
3696
  );
3516
- const endDayIndex = Math.max(
3697
+ const endCol = Math.max(
3517
3698
  0,
3518
- Math.min(6, Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth)),
3699
+ Math.min(
3700
+ this._columnsPerRow - 1,
3701
+ Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth),
3702
+ ),
3519
3703
  );
3520
3704
 
3521
3705
  // Find all weeks that intersect with the selection box
@@ -3533,29 +3717,39 @@ export class CalendarViewElement extends LitElement {
3533
3717
  // Example: Wed 14:00-16:00 across 3 weeks = 3 separate time ranges
3534
3718
 
3535
3719
  for (const week of intersectingWeeks) {
3536
- const weekMinY = Math.max(minY, week.yOffset);
3537
- const weekMaxY = Math.min(maxY, week.yOffset + week.height);
3538
-
3539
- // For each day in the selection
3540
- for (
3541
- let dayIndex = startDayIndex;
3542
- dayIndex <= endDayIndex;
3543
- dayIndex++
3544
- ) {
3720
+ // Check each day to see if it's in the selection box
3721
+ for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
3545
3722
  const day = week.days[dayIndex];
3546
3723
  if (!day) continue;
3547
3724
 
3725
+ const { row, col } = this.getDayVisualPosition(dayIndex);
3726
+
3727
+ // Check if this day's column is in the selection
3728
+ if (col < startCol || col > endCol) continue;
3729
+
3730
+ // Calculate Y bounds for this visual row
3731
+ const rowTop = week.yOffset + row * this.dayHeight;
3732
+ const rowBottom = rowTop + this.dayHeight;
3733
+
3734
+ // Check if selection intersects with this row
3735
+ if (maxY < rowTop || minY > rowBottom) continue;
3736
+
3737
+ const dayMinY = Math.max(minY, rowTop);
3738
+ const dayMaxY = Math.min(maxY, rowBottom);
3739
+
3548
3740
  // Calculate start time for this day
3549
- const dayStartOffset = weekMinY - week.yOffset;
3741
+ const dayStartOffset = dayMinY - rowTop;
3550
3742
  const startMinutes = Math.floor(
3551
- (dayStartOffset / week.height) * 24 * 60,
3743
+ (dayStartOffset / this.dayHeight) * 24 * 60,
3552
3744
  );
3553
3745
  const startHour = Math.floor(startMinutes / 60);
3554
3746
  const startMinute = startMinutes % 60;
3555
3747
 
3556
3748
  // Calculate end time for this day
3557
- const dayEndOffset = weekMaxY - week.yOffset;
3558
- const endMinutes = Math.ceil((dayEndOffset / week.height) * 24 * 60);
3749
+ const dayEndOffset = dayMaxY - rowTop;
3750
+ const endMinutes = Math.ceil(
3751
+ (dayEndOffset / this.dayHeight) * 24 * 60,
3752
+ );
3559
3753
  const endHour = Math.floor(endMinutes / 60);
3560
3754
  const endMinute = endMinutes % 60;
3561
3755
 
@@ -3569,21 +3763,33 @@ export class CalendarViewElement extends LitElement {
3569
3763
  }
3570
3764
  }
3571
3765
  } else {
3572
- // ZOOMED OUT: Create time ranges per WEEK (day range within each week)
3573
- // Example: Wed-Thu across 3 weeks = 3 ranges (Wed 00:00 - Thu 23:59 per week)
3766
+ // ZOOMED OUT: Create time ranges per visible day in selection
3767
+ // For each day whose visual position is in the selection box
3574
3768
 
3575
3769
  for (const week of intersectingWeeks) {
3576
- const startDay = week.days[startDayIndex];
3577
- const endDay = week.days[endDayIndex];
3578
- if (!startDay || !endDay) continue;
3770
+ for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
3771
+ const day = week.days[dayIndex];
3772
+ if (!day) continue;
3579
3773
 
3580
- const rangeStart = new Date(startDay);
3581
- rangeStart.setHours(0, 0, 0, 0);
3774
+ const { row, col } = this.getDayVisualPosition(dayIndex);
3582
3775
 
3583
- const rangeEnd = new Date(endDay);
3584
- rangeEnd.setHours(23, 59, 59, 999);
3776
+ // Check if this day's column is in the selection
3777
+ if (col < startCol || col > endCol) continue;
3585
3778
 
3586
- timeRanges.push({ start: rangeStart, end: rangeEnd });
3779
+ // Check if this day's row is in the selection
3780
+ const rowTop = week.yOffset + row * this.dayHeight;
3781
+ const rowBottom = rowTop + this.dayHeight;
3782
+
3783
+ if (maxY < rowTop || minY > rowBottom) continue;
3784
+
3785
+ const rangeStart = new Date(day);
3786
+ rangeStart.setHours(0, 0, 0, 0);
3787
+
3788
+ const rangeEnd = new Date(day);
3789
+ rangeEnd.setHours(23, 59, 59, 999);
3790
+
3791
+ timeRanges.push({ start: rangeStart, end: rangeEnd });
3792
+ }
3587
3793
  }
3588
3794
  }
3589
3795
 
@@ -3661,6 +3867,8 @@ export class CalendarViewElement extends LitElement {
3661
3867
 
3662
3868
  this.renderCanvas();
3663
3869
 
3870
+ this.repositionEventDetailOverlay();
3871
+
3664
3872
  this.checkAndExtendRange();
3665
3873
 
3666
3874
  this.saveScrollPosition();
@@ -3761,14 +3969,12 @@ export class CalendarViewElement extends LitElement {
3761
3969
  this.internal.setFilter(input.value);
3762
3970
  };
3763
3971
 
3764
- onCalDAVClick = (): void => {
3765
- this.dispatchEvent(new CustomEvent("caldav-config", { bubbles: true }));
3766
- };
3767
-
3768
3972
  onNotificationPopoverToggle = async (): Promise<void> => {
3769
3973
  this.notificationPopoverOpen = !this.notificationPopoverOpen;
3770
3974
  if (this.notificationPopoverOpen) {
3771
- this.dispatchEvent(new CustomEvent("load-notifications", { bubbles: true }));
3975
+ this.dispatchEvent(
3976
+ new CustomEvent("load-notifications", { bubbles: true }),
3977
+ );
3772
3978
  }
3773
3979
  this.requestUpdate();
3774
3980
  };
@@ -3778,7 +3984,6 @@ export class CalendarViewElement extends LitElement {
3778
3984
  this.requestUpdate();
3779
3985
  };
3780
3986
 
3781
-
3782
3987
  onThemeChange = (e: Event): void => {
3783
3988
  const select = e.target as HTMLSelectElement;
3784
3989
  const theme = select.value as ThemeName;
@@ -3813,8 +4018,6 @@ export class CalendarViewElement extends LitElement {
3813
4018
  async onEventClick(event: CalendarEvent, e: MouseEvent): Promise<void> {
3814
4019
  const isCmdOrCtrl = e.metaKey || e.ctrlKey;
3815
4020
 
3816
- console.log(event);
3817
-
3818
4021
  if (isCmdOrCtrl) {
3819
4022
  this.internal.selectEvent(event, "toggle");
3820
4023
  } else {
@@ -4091,17 +4294,21 @@ export class CalendarViewElement extends LitElement {
4091
4294
  const fontFamily = getComputedStyle(this).fontFamily;
4092
4295
  const scrollRect = this.scrollContainer.getBoundingClientRect();
4093
4296
  const gridWidth = scrollRect.width - LEFT_GUTTER_WIDTH - MINIMAP_WIDTH;
4094
- const dayWidth = gridWidth / 7;
4297
+ const dayWidth = gridWidth / this._columnsPerRow;
4095
4298
 
4096
4299
  const getTimeY = (day: Date, hours: number, minutes: number) => {
4097
4300
  const week = this.weeks.find((w) =>
4098
4301
  w.days.some((d) => d.toDateString() === day.toDateString()),
4099
4302
  );
4100
4303
  if (!week) return null;
4101
- const totalMinutes = hours * 60 + minutes;
4102
- return (
4103
- week.yOffset + (totalMinutes / 1440) * week.height - this.scrollTop
4304
+ const dayIndex = week.days.findIndex(
4305
+ (d) => d.toDateString() === day.toDateString(),
4104
4306
  );
4307
+ if (dayIndex < 0) return null;
4308
+ const { row } = this.getDayVisualPosition(dayIndex);
4309
+ const totalMinutes = hours * 60 + minutes;
4310
+ const rowY = week.yOffset + row * this.dayHeight;
4311
+ return rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop;
4105
4312
  };
4106
4313
 
4107
4314
  const getDayColumnX = (day: Date) => {
@@ -4113,7 +4320,8 @@ export class CalendarViewElement extends LitElement {
4113
4320
  (d) => d.toDateString() === day.toDateString(),
4114
4321
  );
4115
4322
  if (dayIndex < 0) return null;
4116
- return dayIndex * dayWidth;
4323
+ const { col } = this.getDayVisualPosition(dayIndex);
4324
+ return col * dayWidth;
4117
4325
  };
4118
4326
 
4119
4327
  const drawBlock = (colX: number, top: number, bottom: number) => {
@@ -4255,7 +4463,7 @@ export class CalendarViewElement extends LitElement {
4255
4463
 
4256
4464
  ctx.clearRect(0, 0, width, height);
4257
4465
 
4258
- const dayWidth = width / 7;
4466
+ const dayWidth = width / this._columnsPerRow;
4259
4467
  const scrollTop = this.scrollTop;
4260
4468
  const fontFamily = getComputedStyle(this).fontFamily;
4261
4469
 
@@ -4275,16 +4483,18 @@ export class CalendarViewElement extends LitElement {
4275
4483
  for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
4276
4484
  const day = week.days[dayIndex];
4277
4485
  if (!day) continue;
4278
- const x = dayIndex * dayWidth;
4279
- const dayTop = week.yOffset - scrollTop;
4280
- const dayBottom = dayTop + week.height;
4486
+
4487
+ const { row, col } = this.getDayVisualPosition(dayIndex);
4488
+ const x = col * dayWidth;
4489
+ const dayTop = week.yOffset + row * this.dayHeight - scrollTop;
4490
+ const dayBottom = dayTop + this.dayHeight;
4281
4491
 
4282
4492
  if (dayIndex === 5 || dayIndex === 6) {
4283
4493
  const bgWeekend =
4284
4494
  getComputedStyle(this).getPropertyValue("--bg-weekend").trim() ||
4285
4495
  "rgba(255, 255, 255, 0.03)";
4286
4496
  ctx.fillStyle = bgWeekend;
4287
- ctx.fillRect(x, dayTop, dayWidth, week.height);
4497
+ ctx.fillRect(x, dayTop, dayWidth, this.dayHeight);
4288
4498
  }
4289
4499
 
4290
4500
  // Draw red outline for current day in zoomed-out view
@@ -4297,14 +4507,14 @@ export class CalendarViewElement extends LitElement {
4297
4507
  .trim() || "rgba(255, 0, 0, 0.8)";
4298
4508
  ctx.strokeStyle = accentTime;
4299
4509
  ctx.lineWidth = 1;
4300
- ctx.strokeRect(x + 1, dayTop + 1, dayWidth - 2, week.height - 2);
4510
+ ctx.strokeRect(x + 1, dayTop + 1, dayWidth - 2, this.dayHeight - 2);
4301
4511
  ctx.lineWidth = 1;
4302
4512
  }
4303
4513
 
4304
4514
  const labelHeight = 12;
4305
4515
  const labelBottomMargin = 64;
4306
4516
  const labelY = Math.min(
4307
- dayBottom - labelHeight,
4517
+ dayBottom - labelHeight - 8,
4308
4518
  height - labelHeight - labelBottomMargin,
4309
4519
  );
4310
4520
 
@@ -4316,6 +4526,78 @@ export class CalendarViewElement extends LitElement {
4316
4526
  }
4317
4527
  }
4318
4528
 
4529
+ renderWeekdayLabels(
4530
+ ctx: CanvasRenderingContext2D,
4531
+ dayWidth: number,
4532
+ visibleWeeks: WeekInfo[],
4533
+ scrollTop: number,
4534
+ height: number,
4535
+ ): void {
4536
+ if (visibleWeeks.length === 0) return;
4537
+
4538
+ const weekdayNames = this.internal.getWeekdayNames();
4539
+ const fontFamily = getComputedStyle(this).fontFamily;
4540
+ const textMuted =
4541
+ getComputedStyle(this).getPropertyValue("--text-muted").trim() ||
4542
+ "rgba(255, 255, 255, 0.4)";
4543
+ const bgPrimary =
4544
+ getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
4545
+ "rgba(30, 30, 30, 0.9)";
4546
+
4547
+ ctx.font = `500 12px ${fontFamily}`;
4548
+ ctx.textAlign = "center";
4549
+ ctx.textBaseline = "top";
4550
+
4551
+ const labelHeight = 16;
4552
+ const labelY = 12; // Below month label
4553
+
4554
+ // Find the first visible visual row
4555
+ const firstWeek = visibleWeeks[0];
4556
+ if (!firstWeek) return;
4557
+
4558
+ // Determine which visual rows are visible
4559
+ for (let row = 0; row < this.rowsPerWeek; row++) {
4560
+ const rowTop = firstWeek.yOffset + row * this.dayHeight - scrollTop;
4561
+ const rowBottom = rowTop + this.dayHeight;
4562
+
4563
+ // Check if this visual row is visible
4564
+ if (rowBottom < 0 || rowTop > height) continue;
4565
+
4566
+ // Calculate sticky Y position - stays at top but doesn't go past row bottom
4567
+ const stickyY = Math.min(labelY, rowBottom - labelHeight - 2);
4568
+ if (stickyY < 0) continue;
4569
+
4570
+ // Draw weekday labels for each column in this visual row
4571
+ for (let col = 0; col < this._columnsPerRow; col++) {
4572
+ const dayIndex = row * this._columnsPerRow + col;
4573
+ if (dayIndex >= 7) continue;
4574
+ const dayName = weekdayNames[dayIndex];
4575
+ if (!dayName) continue;
4576
+
4577
+ const x = LEFT_GUTTER_WIDTH + col * dayWidth + dayWidth / 2;
4578
+
4579
+ // Draw background pill
4580
+ const textWidth = ctx.measureText(dayName).width;
4581
+ const bgPaddingX = 6;
4582
+ const bgPaddingY = 2;
4583
+ ctx.fillStyle = bgPrimary;
4584
+ ctx.beginPath();
4585
+ ctx.roundRect(
4586
+ x - textWidth / 2 - bgPaddingX,
4587
+ stickyY,
4588
+ textWidth + bgPaddingX * 2,
4589
+ labelHeight,
4590
+ 4,
4591
+ );
4592
+ ctx.fill();
4593
+
4594
+ // Draw text
4595
+ ctx.fillStyle = textMuted;
4596
+ ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
4597
+ }
4598
+ }
4599
+ }
4600
+
4319
4601
  renderSelection(): ReturnType<typeof html> {
4320
4602
  if (!this.selection) return html``;
4321
4603
 
@@ -4512,14 +4794,17 @@ export class CalendarViewElement extends LitElement {
4512
4794
 
4513
4795
  const rect = this.scrollContainer.getBoundingClientRect();
4514
4796
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
4515
- const dayWidth = gridWidth / 7;
4797
+ const dayWidth = gridWidth / this._columnsPerRow;
4516
4798
 
4517
4799
  // Check if X is in the calendar grid area
4518
4800
  if (x < LEFT_GUTTER_WIDTH) return null;
4519
4801
 
4520
- const dayIndex = Math.max(
4802
+ const col = Math.max(
4521
4803
  0,
4522
- Math.min(6, Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth)),
4804
+ Math.min(
4805
+ this._columnsPerRow - 1,
4806
+ Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth),
4807
+ ),
4523
4808
  );
4524
4809
 
4525
4810
  // Find week at Y position
@@ -4529,6 +4814,12 @@ export class CalendarViewElement extends LitElement {
4529
4814
 
4530
4815
  if (!week) return null;
4531
4816
 
4817
+ // Calculate which visual row within the week
4818
+ const rowInWeek = Math.floor((y - week.yOffset) / this.dayHeight);
4819
+ const dayIndex = rowInWeek * this._columnsPerRow + col;
4820
+
4821
+ if (dayIndex < 0 || dayIndex > 6) return null;
4822
+
4532
4823
  const day = week.days[dayIndex];
4533
4824
  if (!day) return null;
4534
4825
 
@@ -4537,9 +4828,8 @@ export class CalendarViewElement extends LitElement {
4537
4828
  let minute = 0;
4538
4829
 
4539
4830
  if (this.dayHeight >= 200) {
4540
- const offsetInWeek =
4541
- yOffsetInWeek !== undefined ? yOffsetInWeek : y - week.yOffset;
4542
- const minutes = Math.floor((offsetInWeek / week.height) * 24 * 60);
4831
+ const offsetInRow = y - (week.yOffset + rowInWeek * this.dayHeight);
4832
+ const minutes = Math.floor((offsetInRow / this.dayHeight) * 24 * 60);
4543
4833
  hour = Math.floor(minutes / 60);
4544
4834
  minute = minutes % 60;
4545
4835
  } else {
@@ -4558,7 +4848,7 @@ export class CalendarViewElement extends LitElement {
4558
4848
 
4559
4849
  const rect = this.scrollContainer.getBoundingClientRect();
4560
4850
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
4561
- const dayWidth = gridWidth / 7;
4851
+ const dayWidth = gridWidth / this._columnsPerRow;
4562
4852
 
4563
4853
  // Find the week that contains this date
4564
4854
  const dateStr = date.toDateString();
@@ -4572,15 +4862,18 @@ export class CalendarViewElement extends LitElement {
4572
4862
  const dayIndex = week.days.findIndex((d) => d.toDateString() === dateStr);
4573
4863
  if (dayIndex === -1) return null;
4574
4864
 
4865
+ // Get visual position
4866
+ const { row, col } = this.getDayVisualPosition(dayIndex);
4867
+
4575
4868
  // Calculate X position
4576
- const x = LEFT_GUTTER_WIDTH + dayIndex * dayWidth + dayWidth / 2;
4869
+ const x = LEFT_GUTTER_WIDTH + col * dayWidth + dayWidth / 2;
4577
4870
 
4578
4871
  // Calculate Y position based on time
4579
4872
  const hours = date.getHours();
4580
4873
  const minutes = date.getMinutes();
4581
4874
  const totalMinutes = hours * 60 + minutes;
4582
- const offsetInWeek = (totalMinutes / (24 * 60)) * week.height;
4583
- const y = week.yOffset + offsetInWeek;
4875
+ const offsetInRow = (totalMinutes / (24 * 60)) * this.dayHeight;
4876
+ const y = week.yOffset + row * this.dayHeight + offsetInRow;
4584
4877
 
4585
4878
  return { x, y };
4586
4879
  }
@@ -4669,7 +4962,8 @@ export class CalendarViewElement extends LitElement {
4669
4962
  return html`<div class="notification-empty-state">No notifications set</div>`;
4670
4963
  }
4671
4964
 
4672
- return event.reminders.map(notif => html`
4965
+ return event.reminders.map(
4966
+ (notif) => html`
4673
4967
  <div class="notification-item">
4674
4968
  <select
4675
4969
  class="notification-select"
@@ -4679,14 +4973,20 @@ export class CalendarViewElement extends LitElement {
4679
4973
  this.updateNotification(event, notif.id, { triggerOffset: offset });
4680
4974
  }}
4681
4975
  >
4682
- ${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
+ )}
4683
4982
  </select>
4684
4983
  <button
4685
4984
  class="notification-remove-button"
4686
4985
  @click=${() => this.removeNotification(event, notif.id)}
4687
4986
  >×</button>
4688
4987
  </div>
4689
- `);
4988
+ `,
4989
+ );
4690
4990
  }
4691
4991
 
4692
4992
  addNotification(event: CalendarEvent) {
@@ -4696,31 +4996,41 @@ export class CalendarViewElement extends LitElement {
4696
4996
  enabled: true,
4697
4997
  };
4698
4998
  const reminders = [...(event.reminders || []), newNotif];
4699
- this.dispatchEvent(new CustomEvent("update-event", {
4700
- detail: { event, updates: { reminders } },
4701
- bubbles: true,
4702
- composed: true,
4703
- }));
4999
+ this.dispatchEvent(
5000
+ new CustomEvent("update-event", {
5001
+ detail: { event, updates: { reminders } },
5002
+ bubbles: true,
5003
+ composed: true,
5004
+ }),
5005
+ );
4704
5006
  }
4705
5007
 
4706
- updateNotification(event: CalendarEvent, notifId: string, updates: { triggerOffset: number }) {
4707
- const reminders = (event.reminders || []).map(n =>
4708
- 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
+ }),
4709
5022
  );
4710
- this.dispatchEvent(new CustomEvent("update-event", {
4711
- detail: { event, updates: { reminders } },
4712
- bubbles: true,
4713
- composed: true,
4714
- }));
4715
5023
  }
4716
5024
 
4717
5025
  removeNotification(event: CalendarEvent, notifId: string) {
4718
- const reminders = (event.reminders || []).filter(n => n.id !== notifId);
4719
- this.dispatchEvent(new CustomEvent("update-event", {
4720
- detail: { event, updates: { reminders } },
4721
- bubbles: true,
4722
- composed: true,
4723
- }));
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
+ );
4724
5034
  }
4725
5035
 
4726
5036
  shouldRenderEventWithStripes(event: CalendarEvent): boolean {
@@ -4809,9 +5119,13 @@ export class CalendarViewElement extends LitElement {
4809
5119
  const containerHeight = this.scrollContainer?.clientHeight || 600;
4810
5120
 
4811
5121
  // Try positioning to the right, fallback to left if no space
4812
- const rightPosition = this.selectedEventRect.x + this.selectedEventRect.width + GAP;
5122
+ const rightPosition =
5123
+ this.selectedEventRect.x + this.selectedEventRect.width + GAP;
4813
5124
  const leftPosition = this.selectedEventRect.x - OVERLAY_WIDTH - GAP;
4814
- const preferredLeft = rightPosition + OVERLAY_WIDTH <= containerWidth ? rightPosition : leftPosition;
5125
+ const preferredLeft =
5126
+ rightPosition + OVERLAY_WIDTH <= containerWidth
5127
+ ? rightPosition
5128
+ : leftPosition;
4815
5129
 
4816
5130
  // Clamp to viewport boundaries
4817
5131
  const minLeft = LEFT_GUTTER_WIDTH + GAP;
@@ -4829,24 +5143,22 @@ export class CalendarViewElement extends LitElement {
4829
5143
 
4830
5144
  return html`
4831
5145
  <div class="event-detail-overlay" style="${style}">
4832
- <div class="event-detail-color-bar" style="opacity: ${
4833
- event.readOnly ? "0.5" : "1"
4834
- }"></div>
4835
-
4836
5146
  <div class="event-detail-content">
4837
5147
  <div class="event-detail-header">
4838
5148
  <div class="event-detail-header-content">
4839
5149
  ${
4840
- event.calendar
4841
- ? html`
5150
+ event.calendar
5151
+ ? html`
4842
5152
  <div class="event-detail-section event-detail-calendar">
4843
5153
  <div class="event-detail-value">${event.calendar}</div>
4844
5154
  </div>
4845
5155
  `
4846
- : null
4847
- }
5156
+ : null
5157
+ }
4848
5158
  ${
4849
- event.readOnly
5159
+ event.readOnly ||
5160
+ (event.organizer != null &&
5161
+ !this.currentUserEmails.has(event.organizer.email))
4850
5162
  ? html`
4851
5163
  <h3 class="event-detail-title">${
4852
5164
  event.rrule ? html`<span style="opacity: 0.6">⟳</span> ` : ""
@@ -5004,21 +5316,32 @@ export class CalendarViewElement extends LitElement {
5004
5316
  : null;
5005
5317
  })()}
5006
5318
 
5007
- ${!event.readOnly ? html`
5319
+ ${
5320
+ !event.readOnly
5321
+ ? html`
5008
5322
  <div class="event-detail-section">
5009
- <div class="event-detail-label">Notifications</div>
5323
+ <div class="event-detail-label">
5324
+ <span>Notifications</span>
5325
+
5326
+ <button class="notification-add-button" title="Add notification" @click=${() =>
5327
+ this.addNotification(event)}>
5328
+ +
5329
+ </button>
5330
+ </div>
5010
5331
  <div class="event-notifications">
5011
5332
  ${this.renderNotificationsList(event)}
5012
- <button class="notification-add-button" @click=${() => this.addNotification(event)}>
5013
- + Add notification
5014
- </button>
5015
5333
  </div>
5016
5334
  </div>
5017
- ` : null}
5335
+ `
5336
+ : null
5337
+ }
5018
5338
 
5019
5339
  ${
5020
- event.description
5021
- ? html`
5340
+ event.readOnly ||
5341
+ (event.organizer != null &&
5342
+ !this.currentUserEmails.has(event.organizer.email))
5343
+ ? event.description
5344
+ ? html`
5022
5345
  <div class="event-detail-section">
5023
5346
  <div class="event-detail-label">Description</div>
5024
5347
  <div class="event-detail-description ${
@@ -5048,7 +5371,58 @@ export class CalendarViewElement extends LitElement {
5048
5371
  }
5049
5372
  </div>
5050
5373
  `
5051
- : null
5374
+ : null
5375
+ : html`
5376
+ <div class="event-detail-section">
5377
+ <div class="event-detail-label">Description</div>
5378
+ <textarea
5379
+ class="event-detail-description-input"
5380
+ .value=${event.description ?? ""}
5381
+ placeholder="Add description..."
5382
+ rows="3"
5383
+ @input=${(e: Event) => {
5384
+ const input = e.target as HTMLTextAreaElement;
5385
+ const newDescription = input.value;
5386
+ if (this.updateEventTimeout) {
5387
+ clearTimeout(this.updateEventTimeout);
5388
+ }
5389
+ this.updateEventTimeout = setTimeout(() => {
5390
+ this.dispatchEvent(
5391
+ new CustomEvent("update-event", {
5392
+ detail: {
5393
+ event,
5394
+ updates: { description: newDescription },
5395
+ },
5396
+ bubbles: true,
5397
+ composed: true,
5398
+ }),
5399
+ );
5400
+ this.updateEventTimeout = null;
5401
+ }, 500);
5402
+ }}
5403
+ @blur=${(e: Event) => {
5404
+ const input = e.target as HTMLTextAreaElement;
5405
+ const newDescription = input.value;
5406
+ if (this.updateEventTimeout) {
5407
+ clearTimeout(this.updateEventTimeout);
5408
+ this.updateEventTimeout = null;
5409
+ }
5410
+ if (newDescription !== (event.description ?? "")) {
5411
+ this.dispatchEvent(
5412
+ new CustomEvent("update-event", {
5413
+ detail: {
5414
+ event,
5415
+ updates: { description: newDescription },
5416
+ },
5417
+ bubbles: true,
5418
+ composed: true,
5419
+ }),
5420
+ );
5421
+ }
5422
+ }}
5423
+ />
5424
+ </div>
5425
+ `
5052
5426
  }
5053
5427
  </div>
5054
5428
 
@@ -5113,15 +5487,12 @@ export class CalendarViewElement extends LitElement {
5113
5487
  <div class="container ${this.isDraggingFile ? "dragging-file" : ""}">
5114
5488
  <div class="toolbar">
5115
5489
  <div class="toolbar-left">
5116
- <button class="toolbar-button" title="CalDAV Sources" @click=${
5117
- this.onCalDAVClick
5118
- }>
5119
- 📅
5120
- </button>
5121
- <button class="toolbar-button" title="Upcoming Notifications" @click=${
5122
- this.onNotificationPopoverToggle
5490
+ <slot name="toolbar-center"></slot>
5491
+
5492
+ <button class="toolbar-button" title="Month" @click=${
5493
+ this.scrollToMonth
5123
5494
  }>
5124
- 🔔
5495
+ Month
5125
5496
  </button>
5126
5497
  <button class="toolbar-button" title="Today" @click=${
5127
5498
  this.scrollToToday
@@ -5140,7 +5511,6 @@ export class CalendarViewElement extends LitElement {
5140
5511
 
5141
5512
  </button>
5142
5513
  <div class="toolbar-zoom">
5143
- <span class="toolbar-zoom-label">Zoom</span>
5144
5514
  <input
5145
5515
  type="range"
5146
5516
  class="toolbar-zoom-slider"
@@ -5175,19 +5545,8 @@ export class CalendarViewElement extends LitElement {
5175
5545
  @input=${this.onFilterInput}
5176
5546
  />
5177
5547
  </div>
5178
-
5179
- <slot name="toolbar-center"></slot>
5180
5548
  </div>
5181
5549
  </div>
5182
-
5183
- <div class="header">
5184
- <div class="header-gutter"></div>
5185
- <div class="weekdays">
5186
- ${this.internal
5187
- .getWeekdayNames()
5188
- .map((name) => html`<div class="weekday">${name}</div>`)}
5189
- </div>
5190
- </div>
5191
5550
 
5192
5551
  <div class="body">
5193
5552
  <div class="calendar-area">
@@ -5243,31 +5602,45 @@ export class CalendarViewElement extends LitElement {
5243
5602
  };
5244
5603
 
5245
5604
  return html`
5246
- <div class="notification-popover-overlay" @click=${this.onNotificationPopoverToggle}></div>
5605
+ <div class="notification-popover-overlay" @click=${
5606
+ this.onNotificationPopoverToggle
5607
+ }></div>
5247
5608
  <div class="notification-popover">
5248
5609
  <div class="notification-popover-header">
5249
5610
  <h3>Upcoming Notifications</h3>
5250
- <button class="notification-popover-close" @click=${this.onNotificationPopoverToggle}>×</button>
5611
+ <button class="notification-popover-close" @click=${
5612
+ this.onNotificationPopoverToggle
5613
+ }>×</button>
5251
5614
  </div>
5252
5615
  <div class="notification-popover-content">
5253
- ${this.scheduledNotifications.length === 0
5254
- ? html`<div class="notification-popover-empty">No scheduled notifications</div>`
5255
- : this.scheduledNotifications.map(
5256
- (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`
5257
5621
  <div class="notification-popover-item">
5258
5622
  <div class="notification-popover-item-header">
5259
- <div class="notification-popover-item-title">${notif.eventTitle}</div>
5260
- <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>
5261
5629
  </div>
5262
5630
  <div class="notification-popover-item-details">
5263
- <div class="notification-popover-item-event-time">📅 ${formatEventTime(notif.eventStart)}</div>
5264
- ${notif.eventLocation
5265
- ? html`<div class="notification-popover-item-location">📍 ${notif.eventLocation}</div>`
5266
- : 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
+ }
5267
5639
  </div>
5268
5640
  </div>
5269
5641
  `,
5270
- )}
5642
+ )
5643
+ }
5271
5644
  </div>
5272
5645
  </div>
5273
5646
  `;