@luckydye/calendar 1.1.2 → 1.1.3

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;
@@ -381,6 +342,18 @@ export class CalendarViewElement extends LitElement {
381
342
  resize: none;
382
343
  }
383
344
 
345
+ .event-detail-description-input {
346
+ font-size: 14px;
347
+ color: inherit;
348
+ width: 100%;
349
+ padding: 0.25rem;
350
+ margin: -0.25rem;
351
+ border: none;
352
+ background: none;
353
+ field-sizing: content;
354
+ resize: none;
355
+ }
356
+
384
357
  .event-detail-title-input:focus {
385
358
  border-color: var(--input-border-focus, rgba(255, 255, 255, 0.3));
386
359
  background: var(--input-bg-focus, rgba(0, 0, 0, 0.3));
@@ -430,6 +403,9 @@ export class CalendarViewElement extends LitElement {
430
403
  text-transform: uppercase;
431
404
  color: var(--text-muted, rgba(255, 255, 255, 0.4));
432
405
  letter-spacing: 0.5px;
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: space-between;
433
409
  }
434
410
 
435
411
  .event-detail-value {
@@ -453,6 +429,11 @@ export class CalendarViewElement extends LitElement {
453
429
  .event-detail-link {
454
430
  color: var(--accent-primary, rgb(100, 150, 255));
455
431
  text-decoration: none;
432
+ white-space: nowrap;
433
+ overflow: hidden;
434
+ text-overflow: ellipsis;
435
+ width: 100%;
436
+ display: block;
456
437
  }
457
438
 
458
439
  .event-detail-link:hover {
@@ -535,10 +516,10 @@ export class CalendarViewElement extends LitElement {
535
516
  border: 1px solid var(--grid-color);
536
517
  border-radius: 4px;
537
518
  color: var(--text-primary);
538
- padding: 6px 12px;
519
+ padding: 4px 8px;
520
+ margin: -4px 0;
539
521
  font-size: 13px;
540
522
  cursor: pointer;
541
- width: 100%;
542
523
  }
543
524
 
544
525
  .notification-add-button:hover {
@@ -846,6 +827,53 @@ export class CalendarViewElement extends LitElement {
846
827
  resizingOriginalEnd: Date | null = null;
847
828
  isResizingEvent = false;
848
829
 
830
+ _columnsPerRow = 7;
831
+ set columnsPerRow(value: number) {
832
+ const clamped = Math.max(1, Math.min(7, Math.floor(value)));
833
+ if (this._columnsPerRow !== clamped) {
834
+ this._columnsPerRow = clamped;
835
+ this.updateWeekOffsets();
836
+ this.renderCanvas();
837
+ this.requestUpdate();
838
+ }
839
+ }
840
+ get columnsPerRow(): number {
841
+ return this._columnsPerRow;
842
+ }
843
+
844
+ get rowsPerWeek(): number {
845
+ return Math.ceil(7 / this._columnsPerRow);
846
+ }
847
+
848
+ getDayVisualPosition(dayIndex: number): { row: number; col: number } {
849
+ const row = Math.floor(dayIndex / this._columnsPerRow);
850
+ const col = dayIndex % this._columnsPerRow;
851
+ return { row, col };
852
+ }
853
+
854
+ getVisualPositionFromCoords(x: number, y: number, gridWidth: number): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
855
+ if (!this.scrollContainer) return null;
856
+
857
+ const dayWidth = gridWidth / this._columnsPerRow;
858
+ const col = Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth);
859
+ if (col < 0 || col >= this._columnsPerRow) return null;
860
+
861
+ const week = this.weeks.find(w =>
862
+ w.height > 0 &&
863
+ y >= w.yOffset &&
864
+ y < w.yOffset + w.height
865
+ );
866
+ if (!week) return null;
867
+
868
+ const rowHeight = this.dayHeight;
869
+ const row = Math.floor((y - week.yOffset) / rowHeight);
870
+ const dayIndex = row * this._columnsPerRow + col;
871
+ if (dayIndex < 0 || dayIndex > 6) return null;
872
+
873
+ const timeFraction = ((y - week.yOffset) % rowHeight) / rowHeight;
874
+ return { dayIndex, timeFraction, weekYOffset: week.yOffset };
875
+ }
876
+
849
877
  // History stack for scroll position and zoom
850
878
  // Index 0 is always the most recent entry
851
879
  historyStack: Array<{ scrollTop: number; dayHeight: number }> = [];
@@ -957,7 +985,34 @@ export class CalendarViewElement extends LitElement {
957
985
  }
958
986
 
959
987
  scrollToToday = (): void => {
960
- this.scrollToDate(new Date(), 0.5, true, true);
988
+ const now = new Date();
989
+
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)),
993
+ );
994
+ if (weekIndex < 0) {
995
+ this.weeks = this.internal.resetRangeAroundDate(now);
996
+ this.updateWeekOffsets();
997
+ weekIndex = this.weeks.findIndex(
998
+ (w) => w.days.some((d) => CalendarInternal.isSameDay(d, now)),
999
+ );
1000
+ }
1001
+
1002
+ let offsetInWeek = 0.5;
1003
+ if (weekIndex >= 0) {
1004
+ const targetWeek = this.weeks[weekIndex];
1005
+ const todayIndex = targetWeek.days.findIndex((d) =>
1006
+ CalendarInternal.isSameDay(d, now),
1007
+ );
1008
+ if (todayIndex >= 0) {
1009
+ const row = Math.floor(todayIndex / this._columnsPerRow);
1010
+ const timeFraction = (now.getHours() + now.getMinutes() / 60) / 24;
1011
+ offsetInWeek = (row + timeFraction) / this.rowsPerWeek;
1012
+ }
1013
+ }
1014
+
1015
+ this.scrollToDate(now, offsetInWeek, true, true);
961
1016
  };
962
1017
 
963
1018
  // History management methods
@@ -1180,7 +1235,6 @@ export class CalendarViewElement extends LitElement {
1180
1235
  window.addEventListener("mousemove", this.onMouseMove);
1181
1236
  window.addEventListener("mouseup", this.onMouseUp);
1182
1237
  window.addEventListener("wheel", this.onWheel, { passive: false });
1183
- window.addEventListener("keydown", this.onKeyDown);
1184
1238
  window.addEventListener("paste", this.onPaste);
1185
1239
  this.addEventListener("dragstart", this.onDragStart);
1186
1240
  this.addEventListener("dragend", this.onDragEnd);
@@ -1193,7 +1247,6 @@ export class CalendarViewElement extends LitElement {
1193
1247
  super.disconnectedCallback();
1194
1248
  document.removeEventListener("mousemove", this.onMouseMove);
1195
1249
  document.removeEventListener("mouseup", this.onMouseUp);
1196
- window.removeEventListener("keydown", this.onKeyDown);
1197
1250
  window.removeEventListener("paste", this.onPaste);
1198
1251
  this.removeEventListener("dragstart", this.onDragStart);
1199
1252
  this.removeEventListener("dragend", this.onDragEnd);
@@ -1323,6 +1376,7 @@ export class CalendarViewElement extends LitElement {
1323
1376
 
1324
1377
  updateWeekOffsets(): void {
1325
1378
  let y = 0;
1379
+ const weekHeight = this.dayHeight * this.rowsPerWeek;
1326
1380
 
1327
1381
  if (this.filter) {
1328
1382
  const filteredEvents = this.events;
@@ -1345,13 +1399,13 @@ export class CalendarViewElement extends LitElement {
1345
1399
  (range) => range.end >= weekStartTime && range.start <= weekEndTime,
1346
1400
  );
1347
1401
 
1348
- week.height = hasEvents ? this.dayHeight : 0;
1402
+ week.height = hasEvents ? weekHeight : 0;
1349
1403
  y += week.height;
1350
1404
  }
1351
1405
  } else {
1352
1406
  for (const week of this.weeks) {
1353
1407
  week.yOffset = y;
1354
- week.height = this.dayHeight;
1408
+ week.height = weekHeight;
1355
1409
  y += week.height;
1356
1410
  }
1357
1411
  }
@@ -1391,21 +1445,40 @@ export class CalendarViewElement extends LitElement {
1391
1445
  }
1392
1446
  }
1393
1447
 
1448
+ this.updateColumnsForViewport();
1394
1449
  this.renderCanvas();
1395
1450
  }
1396
1451
 
1452
+ updateColumnsForViewport(): void {
1453
+ if (!this.scrollContainer) return;
1454
+
1455
+ const rect = this.scrollContainer.getBoundingClientRect();
1456
+ const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
1457
+
1458
+ // Determine optimal columns based on available width
1459
+ // Minimum 60px per day column for usability
1460
+ const minDayWidth = 120;
1461
+ const optimalColumns = Math.max(1, Math.min(7, Math.floor(gridWidth / minDayWidth)));
1462
+
1463
+ if (this._columnsPerRow !== optimalColumns) {
1464
+ this._columnsPerRow = optimalColumns;
1465
+ this.updateWeekOffsets();
1466
+ }
1467
+ }
1468
+
1397
1469
  renderCanvas(): void {
1398
1470
  if (!this.ctx || !this.canvas || !this.scrollContainer) return;
1399
1471
 
1400
1472
  const ctx = this.ctx;
1401
1473
  const width = this.canvas.width / (window.devicePixelRatio || 1);
1402
1474
  const height = this.canvas.height / (window.devicePixelRatio || 1);
1475
+ const fontFamily = getComputedStyle(this).fontFamily;
1403
1476
 
1404
1477
  ctx.clearRect(0, 0, width, height);
1405
1478
 
1406
1479
  const scrollTop = this.scrollTop;
1407
1480
  const gridWidth = width - LEFT_GUTTER_WIDTH;
1408
- const dayWidth = gridWidth / 7;
1481
+ const dayWidth = gridWidth / this._columnsPerRow;
1409
1482
  const today = new Date();
1410
1483
 
1411
1484
  // Find visible weeks
@@ -1423,19 +1496,20 @@ export class CalendarViewElement extends LitElement {
1423
1496
  CalendarInternal.isSameDay(d, today),
1424
1497
  );
1425
1498
  if (todayIndex >= 0) {
1426
- const x = LEFT_GUTTER_WIDTH + todayIndex * dayWidth;
1427
- const y = week.yOffset - scrollTop;
1499
+ const { row, col } = this.getDayVisualPosition(todayIndex);
1500
+ const x = LEFT_GUTTER_WIDTH + col * dayWidth;
1501
+ const dayY = week.yOffset + row * this.dayHeight - scrollTop;
1428
1502
  const bgToday =
1429
1503
  getComputedStyle(this).getPropertyValue("--bg-today").trim() ||
1430
1504
  "rgba(255, 255, 255, 0.05)";
1431
1505
  ctx.fillStyle = bgToday;
1432
- ctx.fillRect(x, y, dayWidth, week.height);
1506
+ ctx.fillRect(x, dayY, dayWidth, this.dayHeight);
1433
1507
 
1434
1508
  if (showTimeScale) {
1435
1509
  // Draw current time indicator line (zoomed in)
1436
1510
  const now = new Date();
1437
1511
  const currentMinutes = now.getHours() * 60 + now.getMinutes();
1438
- const timeY = y + (currentMinutes / 1440) * week.height;
1512
+ const timeY = dayY + (currentMinutes / 1440) * this.dayHeight;
1439
1513
  if (timeY >= 0 && timeY <= height) {
1440
1514
  const accentTime =
1441
1515
  getComputedStyle(this)
@@ -1459,8 +1533,8 @@ export class CalendarViewElement extends LitElement {
1459
1533
  "rgba(255, 255, 255, 0.1)";
1460
1534
  ctx.lineWidth = 1;
1461
1535
 
1462
- // Vertical lines (day separators)
1463
- for (let i = 1; i <= 7; i++) {
1536
+ // Vertical lines (day separators) - draw for each column
1537
+ for (let i = 1; i <= this._columnsPerRow; i++) {
1464
1538
  const x = LEFT_GUTTER_WIDTH + i * dayWidth;
1465
1539
  ctx.beginPath();
1466
1540
  ctx.moveTo(x, 0);
@@ -1541,15 +1615,28 @@ export class CalendarViewElement extends LitElement {
1541
1615
  ctx.lineTo(width, y);
1542
1616
  ctx.stroke();
1543
1617
 
1618
+ // Draw horizontal lines between visual rows within this week
1619
+ for (let row = 1; row < this.rowsPerWeek; row++) {
1620
+ const rowY = y + row * this.dayHeight;
1621
+ if (rowY >= 0 && rowY <= height) {
1622
+ ctx.beginPath();
1623
+ ctx.moveTo(LEFT_GUTTER_WIDTH, rowY);
1624
+ ctx.lineTo(width, rowY);
1625
+ ctx.stroke();
1626
+ }
1627
+ }
1628
+
1544
1629
  // Left gutter: week number and time scale
1545
1630
  const hourLabelOpacity = Math.max(
1546
1631
  0,
1547
1632
  Math.min(1, (this.dayHeight - 300) / 300),
1548
1633
  );
1549
1634
 
1635
+ ctx.font = `500 11px ${fontFamily}`;
1636
+ ctx.textBaseline = "bottom";
1550
1637
  ctx.textAlign = "right";
1551
1638
 
1552
- // Draw hourly lines and labels
1639
+ // Draw hourly lines and labels for each visual row
1553
1640
  const gridColor =
1554
1641
  getComputedStyle(this).getPropertyValue("--grid-color").trim() ||
1555
1642
  "rgba(255, 255, 255, 0.1)";
@@ -1568,19 +1655,22 @@ export class CalendarViewElement extends LitElement {
1568
1655
  `${0.4 * hourLabelOpacity})`,
1569
1656
  );
1570
1657
 
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();
1658
+ for (let row = 0; row < this.rowsPerWeek; row++) {
1659
+ const rowY = y + row * this.dayHeight;
1660
+ for (let hour = 0; hour < 24; hour++) {
1661
+ const hourY = rowY + (hour / 24) * this.dayHeight;
1662
+ if (hourY >= 0 && hourY <= height) {
1663
+ // Hour line
1664
+ ctx.beginPath();
1665
+ ctx.moveTo(LEFT_GUTTER_WIDTH, hourY);
1666
+ ctx.lineTo(width, hourY);
1667
+ ctx.stroke();
1579
1668
 
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);
1669
+ // Hour label (only draw if opacity is significant)
1670
+ if (hourLabelOpacity > 0.1) {
1671
+ const label = `${hour.toString().padStart(2, "0")}:00`;
1672
+ ctx.fillText(label, 48, hourY + 4);
1673
+ }
1584
1674
  }
1585
1675
  }
1586
1676
  }
@@ -1588,13 +1678,14 @@ export class CalendarViewElement extends LitElement {
1588
1678
  // Draw current time indicator in left gutter (only when timescale is visible and today is in this week)
1589
1679
  if (hourLabelOpacity > 0.1) {
1590
1680
  const today = new Date();
1591
- const isTodayInWeek = week.days.some((d) =>
1681
+ const todayIndex = week.days.findIndex((d) =>
1592
1682
  CalendarInternal.isSameDay(d, today),
1593
1683
  );
1594
1684
 
1595
- if (isTodayInWeek) {
1685
+ if (todayIndex >= 0) {
1686
+ const { row } = this.getDayVisualPosition(todayIndex);
1596
1687
  const currentMinutes = today.getHours() * 60 + today.getMinutes();
1597
- const timeY = y + (currentMinutes / 1440) * week.height;
1688
+ const timeY = y + row * this.dayHeight + (currentMinutes / 1440) * this.dayHeight;
1598
1689
 
1599
1690
  if (timeY >= 0 && timeY <= height) {
1600
1691
  const hours = today.getHours().toString().padStart(2, "0");
@@ -1671,6 +1762,10 @@ export class CalendarViewElement extends LitElement {
1671
1762
  this.renderEventsOnCanvas(ctx, scrollTop, height, dayWidth, visibleWeeks);
1672
1763
 
1673
1764
  this.renderDateLabels();
1765
+
1766
+ // Draw sticky weekday labels at top of viewport
1767
+ this.renderWeekdayLabels(ctx, dayWidth, visibleWeeks, scrollTop, height);
1768
+
1674
1769
  if (this.isCreatingEvent) {
1675
1770
  this.renderEventCreationPreview();
1676
1771
  }
@@ -1946,11 +2041,21 @@ export class CalendarViewElement extends LitElement {
1946
2041
  isEnd,
1947
2042
  totalWeeks,
1948
2043
  } = segment;
1949
- const weekHeight = week.height;
1950
2044
  const weekYOffset = week.yOffset;
1951
2045
 
1952
2046
  const allDay = event.isAllDay === true;
1953
2047
 
2048
+ // Get visual position for the start day
2049
+ const startVisualPos = this.getDayVisualPosition(startDayIndex);
2050
+ const endVisualPos = this.getDayVisualPosition(endDayIndex);
2051
+
2052
+ // Skip segments that span multiple visual rows for now (would need to be split)
2053
+ // For simple case, render each day on its visual row
2054
+ if (startVisualPos.row !== endVisualPos.row) {
2055
+ // Event spans visual rows - for now, only render the start portion
2056
+ // TODO: Split into multiple segments across visual rows
2057
+ }
2058
+
1954
2059
  let yStart: number;
1955
2060
  let yEnd: number;
1956
2061
 
@@ -1981,8 +2086,10 @@ export class CalendarViewElement extends LitElement {
1981
2086
  const endMinutes =
1982
2087
  effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
1983
2088
 
1984
- yStart = weekYOffset + (startMinutes / 1440) * weekHeight;
1985
- yEnd = weekYOffset + (endMinutes / 1440) * weekHeight;
2089
+ // Position within the visual row
2090
+ const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
2091
+ yStart = visualRowY + (startMinutes / 1440) * this.dayHeight;
2092
+ yEnd = visualRowY + (endMinutes / 1440) * this.dayHeight;
1986
2093
  } else {
1987
2094
  const eventKey = `${weekIndex}-${event.id}`;
1988
2095
  let rowIndex = eventRowIndex.get(eventKey);
@@ -2015,12 +2122,14 @@ export class CalendarViewElement extends LitElement {
2015
2122
  occupied.add(rowIndex);
2016
2123
  }
2017
2124
 
2018
- const maxEventsInWeek = Math.floor(
2019
- (weekHeight - 4) / (MIN_EVENT_HEIGHT + 2),
2125
+ const maxEventsInRow = Math.floor(
2126
+ (this.dayHeight - 4) / (MIN_EVENT_HEIGHT + 2),
2020
2127
  );
2021
- if (rowIndex >= maxEventsInWeek) continue;
2128
+ if (rowIndex >= maxEventsInRow) continue;
2022
2129
 
2023
- yStart = weekYOffset + 4 + rowIndex * (MIN_EVENT_HEIGHT + 2);
2130
+ // Position within the visual row
2131
+ const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
2132
+ yStart = visualRowY + 4 + rowIndex * (MIN_EVENT_HEIGHT + 2);
2024
2133
  yEnd = yStart + MIN_EVENT_HEIGHT;
2025
2134
  }
2026
2135
 
@@ -2046,13 +2155,15 @@ export class CalendarViewElement extends LitElement {
2046
2155
  const columnWidth = dayWidth / columnLayout.totalColumns;
2047
2156
  x =
2048
2157
  LEFT_GUTTER_WIDTH +
2049
- startDayIndex * dayWidth +
2158
+ startVisualPos.col * dayWidth +
2050
2159
  columnLayout.column * columnWidth;
2051
2160
  spanWidth = columnWidth;
2052
2161
  } else {
2053
- // Normal layout
2054
- x = LEFT_GUTTER_WIDTH + startDayIndex * dayWidth;
2055
- spanWidth = (endDayIndex - startDayIndex + 1) * dayWidth;
2162
+ // Normal layout - use visual column position
2163
+ // For multi-day events on same visual row, calculate span
2164
+ const colSpan = endVisualPos.col - startVisualPos.col + 1;
2165
+ x = LEFT_GUTTER_WIDTH + startVisualPos.col * dayWidth;
2166
+ spanWidth = colSpan * dayWidth;
2056
2167
  }
2057
2168
 
2058
2169
  // Convert to viewport coordinates
@@ -2424,7 +2535,7 @@ export class CalendarViewElement extends LitElement {
2424
2535
  const stickyTop = Math.max(0, scrollTop - labelY);
2425
2536
  const maxStickyTop = nextMonthY - labelY - 24;
2426
2537
  const clampedStickyTop = Math.min(stickyTop, maxStickyTop);
2427
- const labelTopMargin = 0;
2538
+ const labelTopMargin = 32;
2428
2539
  const finalTop = labelY + clampedStickyTop - scrollTop + labelTopMargin;
2429
2540
 
2430
2541
  ctx.save();
@@ -2435,7 +2546,7 @@ export class CalendarViewElement extends LitElement {
2435
2546
  const labelText = `${month.monthName} ${month.year}`;
2436
2547
  const textWidth = ctx.measureText(labelText).width;
2437
2548
  const leftMargin = 8;
2438
- const textX = 0 + padding[3] + leftMargin;
2549
+ const textX = 64 + padding[3] + leftMargin;
2439
2550
  const textY = finalTop + padding[0];
2440
2551
 
2441
2552
  // Draw background
@@ -2953,110 +3064,82 @@ export class CalendarViewElement extends LitElement {
2953
3064
  }
2954
3065
  };
2955
3066
 
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();
3067
+ forceSync(): void {
3068
+ this.dispatchEvent(new CustomEvent("force-sync", { bubbles: true }));
3069
+ }
2975
3070
 
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
- }
3071
+ copySelectedEvents(): void {
3072
+ const selectedEvents = this.internal.getSelectedEvents();
3073
+ if (selectedEvents.length === 0) return;
3074
+
3075
+ const icalText = serializeEventsToICal(selectedEvents);
3076
+ const blob = new Blob([icalText], { type: "text/plain" });
3077
+ const item = new ClipboardItem({ "text/plain": blob });
3078
+ navigator.clipboard.write([item]).then(() => {
3079
+ const count = selectedEvents.length;
3080
+ queueStatus(`Copied ${count} event${count === 1 ? "" : "s"} to clipboard`);
3081
+ });
3082
+ }
2991
3083
 
2992
- const selectedEvents = this.internal.getSelectedEvents();
2993
- if (selectedEvents.length === 0) return;
3084
+ deleteSelectedEvents(): void {
3085
+ const selectedEvents = this.internal.getSelectedEvents();
3086
+ if (selectedEvents.length === 0) return;
2994
3087
 
2995
- // Prevent deleting read-only events
2996
- const deletableEvents = selectedEvents.filter((event) => !event.readOnly);
2997
- if (deletableEvents.length === 0) return;
3088
+ const deletableEvents = selectedEvents.filter((event) => !event.readOnly);
3089
+ if (deletableEvents.length === 0) return;
2998
3090
 
2999
- // Prevent default behavior (like going back in browser)
3000
- e.preventDefault();
3091
+ this.dispatchEvent(
3092
+ new CustomEvent("delete-events", {
3093
+ detail: { events: deletableEvents },
3094
+ bubbles: true,
3095
+ }),
3096
+ );
3001
3097
 
3002
- this.dispatchEvent(
3003
- new CustomEvent("delete-events", {
3004
- detail: { events: deletableEvents },
3005
- bubbles: true,
3006
- }),
3007
- );
3098
+ this.internal.clearSelection();
3099
+ this.selectedEventForDetail = null;
3100
+ this.selectedEventRect = null;
3101
+ this.dispatchEvent(
3102
+ new CustomEvent("selection-change", {
3103
+ detail: { selectedEvents: [] },
3104
+ bubbles: true,
3105
+ }),
3106
+ );
3107
+ }
3008
3108
 
3009
- // Clear selection after delete
3109
+ escape(): void {
3110
+ if (this.isCreatingEvent || this.eventCreationStart) {
3111
+ this.clearEventCreationState();
3112
+ this.renderCanvas();
3113
+ queueStatus("Event creation cancelled");
3114
+ } else if (this.isDraggingEvent || this.movingEvent) {
3115
+ this.resetDragState();
3116
+ this.renderCanvas();
3117
+ queueStatus("Event move cancelled");
3118
+ } else if (this.isResizingEvent) {
3119
+ this.isResizingEvent = false;
3120
+ this.resizeEvent = null;
3121
+ this.resizeStartY = 0;
3122
+ this.resizeEdge = null;
3123
+ this.renderCanvas();
3124
+ queueStatus("Event resize cancelled");
3125
+ } else if (
3126
+ this.internal.getSelectedEvents().length > 0 ||
3127
+ this.selectedEventForDetail
3128
+ ) {
3010
3129
  this.internal.clearSelection();
3011
3130
  this.selectedEventForDetail = null;
3012
3131
  this.selectedEventRect = null;
3132
+ this.isDescriptionExpanded = false;
3133
+ this.renderCanvas();
3134
+ this.requestUpdate();
3013
3135
  this.dispatchEvent(
3014
3136
  new CustomEvent("selection-change", {
3015
3137
  detail: { selectedEvents: [] },
3016
3138
  bubbles: true,
3017
3139
  }),
3018
3140
  );
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
3141
  }
3059
- };
3142
+ }
3060
3143
 
3061
3144
  onScrollContainerMouseMove = (e: MouseEvent): void => {
3062
3145
  if (!this.scrollContainer || this.isDraggingZoom) return;
@@ -3506,16 +3589,16 @@ export class CalendarViewElement extends LitElement {
3506
3589
 
3507
3590
  const rect = this.scrollContainer.getBoundingClientRect();
3508
3591
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
3509
- const dayWidth = gridWidth / 7;
3592
+ const dayWidth = gridWidth / this._columnsPerRow;
3510
3593
 
3511
- // Find day indices from X coordinates
3512
- const startDayIndex = Math.max(
3594
+ // Find column indices from X coordinates
3595
+ const startCol = Math.max(
3513
3596
  0,
3514
- Math.min(6, Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth)),
3597
+ Math.min(this._columnsPerRow - 1, Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth)),
3515
3598
  );
3516
- const endDayIndex = Math.max(
3599
+ const endCol = Math.max(
3517
3600
  0,
3518
- Math.min(6, Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth)),
3601
+ Math.min(this._columnsPerRow - 1, Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth)),
3519
3602
  );
3520
3603
 
3521
3604
  // Find all weeks that intersect with the selection box
@@ -3533,29 +3616,37 @@ export class CalendarViewElement extends LitElement {
3533
3616
  // Example: Wed 14:00-16:00 across 3 weeks = 3 separate time ranges
3534
3617
 
3535
3618
  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
- ) {
3619
+ // Check each day to see if it's in the selection box
3620
+ for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
3545
3621
  const day = week.days[dayIndex];
3546
3622
  if (!day) continue;
3623
+
3624
+ const { row, col } = this.getDayVisualPosition(dayIndex);
3625
+
3626
+ // Check if this day's column is in the selection
3627
+ if (col < startCol || col > endCol) continue;
3628
+
3629
+ // Calculate Y bounds for this visual row
3630
+ const rowTop = week.yOffset + row * this.dayHeight;
3631
+ const rowBottom = rowTop + this.dayHeight;
3632
+
3633
+ // Check if selection intersects with this row
3634
+ if (maxY < rowTop || minY > rowBottom) continue;
3635
+
3636
+ const dayMinY = Math.max(minY, rowTop);
3637
+ const dayMaxY = Math.min(maxY, rowBottom);
3547
3638
 
3548
3639
  // Calculate start time for this day
3549
- const dayStartOffset = weekMinY - week.yOffset;
3640
+ const dayStartOffset = dayMinY - rowTop;
3550
3641
  const startMinutes = Math.floor(
3551
- (dayStartOffset / week.height) * 24 * 60,
3642
+ (dayStartOffset / this.dayHeight) * 24 * 60,
3552
3643
  );
3553
3644
  const startHour = Math.floor(startMinutes / 60);
3554
3645
  const startMinute = startMinutes % 60;
3555
3646
 
3556
3647
  // Calculate end time for this day
3557
- const dayEndOffset = weekMaxY - week.yOffset;
3558
- const endMinutes = Math.ceil((dayEndOffset / week.height) * 24 * 60);
3648
+ const dayEndOffset = dayMaxY - rowTop;
3649
+ const endMinutes = Math.ceil((dayEndOffset / this.dayHeight) * 24 * 60);
3559
3650
  const endHour = Math.floor(endMinutes / 60);
3560
3651
  const endMinute = endMinutes % 60;
3561
3652
 
@@ -3569,21 +3660,33 @@ export class CalendarViewElement extends LitElement {
3569
3660
  }
3570
3661
  }
3571
3662
  } 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)
3663
+ // ZOOMED OUT: Create time ranges per visible day in selection
3664
+ // For each day whose visual position is in the selection box
3574
3665
 
3575
3666
  for (const week of intersectingWeeks) {
3576
- const startDay = week.days[startDayIndex];
3577
- const endDay = week.days[endDayIndex];
3578
- if (!startDay || !endDay) continue;
3579
-
3580
- const rangeStart = new Date(startDay);
3581
- rangeStart.setHours(0, 0, 0, 0);
3667
+ for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
3668
+ const day = week.days[dayIndex];
3669
+ if (!day) continue;
3670
+
3671
+ const { row, col } = this.getDayVisualPosition(dayIndex);
3672
+
3673
+ // Check if this day's column is in the selection
3674
+ if (col < startCol || col > endCol) continue;
3675
+
3676
+ // Check if this day's row is in the selection
3677
+ const rowTop = week.yOffset + row * this.dayHeight;
3678
+ const rowBottom = rowTop + this.dayHeight;
3679
+
3680
+ if (maxY < rowTop || minY > rowBottom) continue;
3681
+
3682
+ const rangeStart = new Date(day);
3683
+ rangeStart.setHours(0, 0, 0, 0);
3582
3684
 
3583
- const rangeEnd = new Date(endDay);
3584
- rangeEnd.setHours(23, 59, 59, 999);
3685
+ const rangeEnd = new Date(day);
3686
+ rangeEnd.setHours(23, 59, 59, 999);
3585
3687
 
3586
- timeRanges.push({ start: rangeStart, end: rangeEnd });
3688
+ timeRanges.push({ start: rangeStart, end: rangeEnd });
3689
+ }
3587
3690
  }
3588
3691
  }
3589
3692
 
@@ -3761,10 +3864,6 @@ export class CalendarViewElement extends LitElement {
3761
3864
  this.internal.setFilter(input.value);
3762
3865
  };
3763
3866
 
3764
- onCalDAVClick = (): void => {
3765
- this.dispatchEvent(new CustomEvent("caldav-config", { bubbles: true }));
3766
- };
3767
-
3768
3867
  onNotificationPopoverToggle = async (): Promise<void> => {
3769
3868
  this.notificationPopoverOpen = !this.notificationPopoverOpen;
3770
3869
  if (this.notificationPopoverOpen) {
@@ -4091,16 +4190,22 @@ export class CalendarViewElement extends LitElement {
4091
4190
  const fontFamily = getComputedStyle(this).fontFamily;
4092
4191
  const scrollRect = this.scrollContainer.getBoundingClientRect();
4093
4192
  const gridWidth = scrollRect.width - LEFT_GUTTER_WIDTH - MINIMAP_WIDTH;
4094
- const dayWidth = gridWidth / 7;
4193
+ const dayWidth = gridWidth / this._columnsPerRow;
4095
4194
 
4096
4195
  const getTimeY = (day: Date, hours: number, minutes: number) => {
4097
4196
  const week = this.weeks.find((w) =>
4098
4197
  w.days.some((d) => d.toDateString() === day.toDateString()),
4099
4198
  );
4100
4199
  if (!week) return null;
4200
+ const dayIndex = week.days.findIndex(
4201
+ (d) => d.toDateString() === day.toDateString(),
4202
+ );
4203
+ if (dayIndex < 0) return null;
4204
+ const { row } = this.getDayVisualPosition(dayIndex);
4101
4205
  const totalMinutes = hours * 60 + minutes;
4206
+ const rowY = week.yOffset + row * this.dayHeight;
4102
4207
  return (
4103
- week.yOffset + (totalMinutes / 1440) * week.height - this.scrollTop
4208
+ rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop
4104
4209
  );
4105
4210
  };
4106
4211
 
@@ -4113,7 +4218,8 @@ export class CalendarViewElement extends LitElement {
4113
4218
  (d) => d.toDateString() === day.toDateString(),
4114
4219
  );
4115
4220
  if (dayIndex < 0) return null;
4116
- return dayIndex * dayWidth;
4221
+ const { col } = this.getDayVisualPosition(dayIndex);
4222
+ return col * dayWidth;
4117
4223
  };
4118
4224
 
4119
4225
  const drawBlock = (colX: number, top: number, bottom: number) => {
@@ -4255,7 +4361,7 @@ export class CalendarViewElement extends LitElement {
4255
4361
 
4256
4362
  ctx.clearRect(0, 0, width, height);
4257
4363
 
4258
- const dayWidth = width / 7;
4364
+ const dayWidth = width / this._columnsPerRow;
4259
4365
  const scrollTop = this.scrollTop;
4260
4366
  const fontFamily = getComputedStyle(this).fontFamily;
4261
4367
 
@@ -4275,16 +4381,18 @@ export class CalendarViewElement extends LitElement {
4275
4381
  for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
4276
4382
  const day = week.days[dayIndex];
4277
4383
  if (!day) continue;
4278
- const x = dayIndex * dayWidth;
4279
- const dayTop = week.yOffset - scrollTop;
4280
- const dayBottom = dayTop + week.height;
4384
+
4385
+ const { row, col } = this.getDayVisualPosition(dayIndex);
4386
+ const x = col * dayWidth;
4387
+ const dayTop = week.yOffset + row * this.dayHeight - scrollTop;
4388
+ const dayBottom = dayTop + this.dayHeight;
4281
4389
 
4282
4390
  if (dayIndex === 5 || dayIndex === 6) {
4283
4391
  const bgWeekend =
4284
4392
  getComputedStyle(this).getPropertyValue("--bg-weekend").trim() ||
4285
4393
  "rgba(255, 255, 255, 0.03)";
4286
4394
  ctx.fillStyle = bgWeekend;
4287
- ctx.fillRect(x, dayTop, dayWidth, week.height);
4395
+ ctx.fillRect(x, dayTop, dayWidth, this.dayHeight);
4288
4396
  }
4289
4397
 
4290
4398
  // Draw red outline for current day in zoomed-out view
@@ -4297,14 +4405,14 @@ export class CalendarViewElement extends LitElement {
4297
4405
  .trim() || "rgba(255, 0, 0, 0.8)";
4298
4406
  ctx.strokeStyle = accentTime;
4299
4407
  ctx.lineWidth = 1;
4300
- ctx.strokeRect(x + 1, dayTop + 1, dayWidth - 2, week.height - 2);
4408
+ ctx.strokeRect(x + 1, dayTop + 1, dayWidth - 2, this.dayHeight - 2);
4301
4409
  ctx.lineWidth = 1;
4302
4410
  }
4303
4411
 
4304
4412
  const labelHeight = 12;
4305
4413
  const labelBottomMargin = 64;
4306
4414
  const labelY = Math.min(
4307
- dayBottom - labelHeight,
4415
+ dayBottom - labelHeight - 8,
4308
4416
  height - labelHeight - labelBottomMargin,
4309
4417
  );
4310
4418
 
@@ -4316,6 +4424,78 @@ export class CalendarViewElement extends LitElement {
4316
4424
  }
4317
4425
  }
4318
4426
 
4427
+ renderWeekdayLabels(
4428
+ ctx: CanvasRenderingContext2D,
4429
+ dayWidth: number,
4430
+ visibleWeeks: WeekInfo[],
4431
+ scrollTop: number,
4432
+ height: number,
4433
+ ): void {
4434
+ if (visibleWeeks.length === 0) return;
4435
+
4436
+ const weekdayNames = this.internal.getWeekdayNames();
4437
+ const fontFamily = getComputedStyle(this).fontFamily;
4438
+ const textMuted =
4439
+ getComputedStyle(this).getPropertyValue("--text-muted").trim() ||
4440
+ "rgba(255, 255, 255, 0.4)";
4441
+ const bgPrimary =
4442
+ getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
4443
+ "rgba(30, 30, 30, 0.9)";
4444
+
4445
+ ctx.font = `500 12px ${fontFamily}`;
4446
+ ctx.textAlign = "center";
4447
+ ctx.textBaseline = "top";
4448
+
4449
+ const labelHeight = 16;
4450
+ const labelY = 12; // Below month label
4451
+
4452
+ // Find the first visible visual row
4453
+ const firstWeek = visibleWeeks[0];
4454
+ if (!firstWeek) return;
4455
+
4456
+ // Determine which visual rows are visible
4457
+ for (let row = 0; row < this.rowsPerWeek; row++) {
4458
+ const rowTop = firstWeek.yOffset + row * this.dayHeight - scrollTop;
4459
+ const rowBottom = rowTop + this.dayHeight;
4460
+
4461
+ // Check if this visual row is visible
4462
+ if (rowBottom < 0 || rowTop > height) continue;
4463
+
4464
+ // Calculate sticky Y position - stays at top but doesn't go past row bottom
4465
+ const stickyY = Math.min(labelY, rowBottom - labelHeight - 2);
4466
+ if (stickyY < 0) continue;
4467
+
4468
+ // Draw weekday labels for each column in this visual row
4469
+ for (let col = 0; col < this._columnsPerRow; col++) {
4470
+ const dayIndex = row * this._columnsPerRow + col;
4471
+ if (dayIndex >= 7) continue;
4472
+ const dayName = weekdayNames[dayIndex];
4473
+ if (!dayName) continue;
4474
+
4475
+ const x = LEFT_GUTTER_WIDTH + col * dayWidth + dayWidth / 2;
4476
+
4477
+ // Draw background pill
4478
+ const textWidth = ctx.measureText(dayName).width;
4479
+ const bgPaddingX = 6;
4480
+ const bgPaddingY = 2;
4481
+ ctx.fillStyle = bgPrimary;
4482
+ ctx.beginPath();
4483
+ ctx.roundRect(
4484
+ x - textWidth / 2 - bgPaddingX,
4485
+ stickyY,
4486
+ textWidth + bgPaddingX * 2,
4487
+ labelHeight,
4488
+ 4,
4489
+ );
4490
+ ctx.fill();
4491
+
4492
+ // Draw text
4493
+ ctx.fillStyle = textMuted;
4494
+ ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
4495
+ }
4496
+ }
4497
+ }
4498
+
4319
4499
  renderSelection(): ReturnType<typeof html> {
4320
4500
  if (!this.selection) return html``;
4321
4501
 
@@ -4512,14 +4692,14 @@ export class CalendarViewElement extends LitElement {
4512
4692
 
4513
4693
  const rect = this.scrollContainer.getBoundingClientRect();
4514
4694
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
4515
- const dayWidth = gridWidth / 7;
4695
+ const dayWidth = gridWidth / this._columnsPerRow;
4516
4696
 
4517
4697
  // Check if X is in the calendar grid area
4518
4698
  if (x < LEFT_GUTTER_WIDTH) return null;
4519
4699
 
4520
- const dayIndex = Math.max(
4700
+ const col = Math.max(
4521
4701
  0,
4522
- Math.min(6, Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth)),
4702
+ Math.min(this._columnsPerRow - 1, Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth)),
4523
4703
  );
4524
4704
 
4525
4705
  // Find week at Y position
@@ -4529,6 +4709,12 @@ export class CalendarViewElement extends LitElement {
4529
4709
 
4530
4710
  if (!week) return null;
4531
4711
 
4712
+ // Calculate which visual row within the week
4713
+ const rowInWeek = Math.floor((y - week.yOffset) / this.dayHeight);
4714
+ const dayIndex = rowInWeek * this._columnsPerRow + col;
4715
+
4716
+ if (dayIndex < 0 || dayIndex > 6) return null;
4717
+
4532
4718
  const day = week.days[dayIndex];
4533
4719
  if (!day) return null;
4534
4720
 
@@ -4537,9 +4723,8 @@ export class CalendarViewElement extends LitElement {
4537
4723
  let minute = 0;
4538
4724
 
4539
4725
  if (this.dayHeight >= 200) {
4540
- const offsetInWeek =
4541
- yOffsetInWeek !== undefined ? yOffsetInWeek : y - week.yOffset;
4542
- const minutes = Math.floor((offsetInWeek / week.height) * 24 * 60);
4726
+ const offsetInRow = y - (week.yOffset + rowInWeek * this.dayHeight);
4727
+ const minutes = Math.floor((offsetInRow / this.dayHeight) * 24 * 60);
4543
4728
  hour = Math.floor(minutes / 60);
4544
4729
  minute = minutes % 60;
4545
4730
  } else {
@@ -4558,7 +4743,7 @@ export class CalendarViewElement extends LitElement {
4558
4743
 
4559
4744
  const rect = this.scrollContainer.getBoundingClientRect();
4560
4745
  const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
4561
- const dayWidth = gridWidth / 7;
4746
+ const dayWidth = gridWidth / this._columnsPerRow;
4562
4747
 
4563
4748
  // Find the week that contains this date
4564
4749
  const dateStr = date.toDateString();
@@ -4572,15 +4757,18 @@ export class CalendarViewElement extends LitElement {
4572
4757
  const dayIndex = week.days.findIndex((d) => d.toDateString() === dateStr);
4573
4758
  if (dayIndex === -1) return null;
4574
4759
 
4760
+ // Get visual position
4761
+ const { row, col } = this.getDayVisualPosition(dayIndex);
4762
+
4575
4763
  // Calculate X position
4576
- const x = LEFT_GUTTER_WIDTH + dayIndex * dayWidth + dayWidth / 2;
4764
+ const x = LEFT_GUTTER_WIDTH + col * dayWidth + dayWidth / 2;
4577
4765
 
4578
4766
  // Calculate Y position based on time
4579
4767
  const hours = date.getHours();
4580
4768
  const minutes = date.getMinutes();
4581
4769
  const totalMinutes = hours * 60 + minutes;
4582
- const offsetInWeek = (totalMinutes / (24 * 60)) * week.height;
4583
- const y = week.yOffset + offsetInWeek;
4770
+ const offsetInRow = (totalMinutes / (24 * 60)) * this.dayHeight;
4771
+ const y = week.yOffset + row * this.dayHeight + offsetInRow;
4584
4772
 
4585
4773
  return { x, y };
4586
4774
  }
@@ -4846,7 +5034,7 @@ export class CalendarViewElement extends LitElement {
4846
5034
  : null
4847
5035
  }
4848
5036
  ${
4849
- event.readOnly
5037
+ (event.readOnly || (event.organizer != null && !this.currentUserEmails.has(event.organizer.email)))
4850
5038
  ? html`
4851
5039
  <h3 class="event-detail-title">${
4852
5040
  event.rrule ? html`<span style="opacity: 0.6">⟳</span> ` : ""
@@ -5006,19 +5194,23 @@ export class CalendarViewElement extends LitElement {
5006
5194
 
5007
5195
  ${!event.readOnly ? html`
5008
5196
  <div class="event-detail-section">
5009
- <div class="event-detail-label">Notifications</div>
5197
+ <div class="event-detail-label">
5198
+ <span>Notifications</span>
5199
+
5200
+ <button class="notification-add-button" title="Add notification" @click=${() => this.addNotification(event)}>
5201
+ +
5202
+ </button>
5203
+ </div>
5010
5204
  <div class="event-notifications">
5011
5205
  ${this.renderNotificationsList(event)}
5012
- <button class="notification-add-button" @click=${() => this.addNotification(event)}>
5013
- + Add notification
5014
- </button>
5015
5206
  </div>
5016
5207
  </div>
5017
5208
  ` : null}
5018
5209
 
5019
5210
  ${
5020
- event.description
5021
- ? html`
5211
+ (event.readOnly || (event.organizer != null && !this.currentUserEmails.has(event.organizer.email)))
5212
+ ? event.description
5213
+ ? html`
5022
5214
  <div class="event-detail-section">
5023
5215
  <div class="event-detail-label">Description</div>
5024
5216
  <div class="event-detail-description ${
@@ -5048,7 +5240,52 @@ export class CalendarViewElement extends LitElement {
5048
5240
  }
5049
5241
  </div>
5050
5242
  `
5051
- : null
5243
+ : null
5244
+ : html`
5245
+ <div class="event-detail-section">
5246
+ <div class="event-detail-label">Description</div>
5247
+ <textarea
5248
+ class="event-detail-description-input"
5249
+ .value=${event.description ?? ""}
5250
+ placeholder="Add description..."
5251
+ rows="3"
5252
+ @input=${(e: Event) => {
5253
+ const input = e.target as HTMLTextAreaElement;
5254
+ const newDescription = input.value;
5255
+ if (this.updateEventTimeout) {
5256
+ clearTimeout(this.updateEventTimeout);
5257
+ }
5258
+ this.updateEventTimeout = setTimeout(() => {
5259
+ this.dispatchEvent(
5260
+ new CustomEvent("update-event", {
5261
+ detail: { event, updates: { description: newDescription } },
5262
+ bubbles: true,
5263
+ composed: true,
5264
+ }),
5265
+ );
5266
+ this.updateEventTimeout = null;
5267
+ }, 500);
5268
+ }}
5269
+ @blur=${(e: Event) => {
5270
+ const input = e.target as HTMLTextAreaElement;
5271
+ const newDescription = input.value;
5272
+ if (this.updateEventTimeout) {
5273
+ clearTimeout(this.updateEventTimeout);
5274
+ this.updateEventTimeout = null;
5275
+ }
5276
+ if (newDescription !== (event.description ?? "")) {
5277
+ this.dispatchEvent(
5278
+ new CustomEvent("update-event", {
5279
+ detail: { event, updates: { description: newDescription } },
5280
+ bubbles: true,
5281
+ composed: true,
5282
+ }),
5283
+ );
5284
+ }
5285
+ }}
5286
+ />
5287
+ </div>
5288
+ `
5052
5289
  }
5053
5290
  </div>
5054
5291
 
@@ -5113,16 +5350,8 @@ export class CalendarViewElement extends LitElement {
5113
5350
  <div class="container ${this.isDraggingFile ? "dragging-file" : ""}">
5114
5351
  <div class="toolbar">
5115
5352
  <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
5123
- }>
5124
- 🔔
5125
- </button>
5353
+ <slot name="toolbar-center"></slot>
5354
+
5126
5355
  <button class="toolbar-button" title="Today" @click=${
5127
5356
  this.scrollToToday
5128
5357
  }>
@@ -5140,7 +5369,6 @@ export class CalendarViewElement extends LitElement {
5140
5369
 
5141
5370
  </button>
5142
5371
  <div class="toolbar-zoom">
5143
- <span class="toolbar-zoom-label">Zoom</span>
5144
5372
  <input
5145
5373
  type="range"
5146
5374
  class="toolbar-zoom-slider"
@@ -5175,19 +5403,8 @@ export class CalendarViewElement extends LitElement {
5175
5403
  @input=${this.onFilterInput}
5176
5404
  />
5177
5405
  </div>
5178
-
5179
- <slot name="toolbar-center"></slot>
5180
5406
  </div>
5181
5407
  </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
5408
 
5192
5409
  <div class="body">
5193
5410
  <div class="calendar-area">