@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.
- package/dist/calendar.js +1387 -1301
- package/package.json +1 -1
- package/src/CalDAVConfig.ts +25 -0
- package/src/CalendarIntegration.ts +1 -2
- package/src/CalendarInternal.ts +20 -1
- package/src/CalendarView.ts +474 -257
- package/src/GoogleCalendarSource.ts +0 -7
- package/src/InMemorySource.ts +0 -15
- package/src/InhouseBookingSource.ts +331 -46
- package/src/Keybinds.ts +46 -0
- package/src/app.css +28 -0
- package/src/app.ts +173 -63
package/src/CalendarView.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
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 /
|
|
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
|
|
1427
|
-
const
|
|
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,
|
|
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 =
|
|
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 <=
|
|
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
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
|
1681
|
+
const todayIndex = week.days.findIndex((d) =>
|
|
1592
1682
|
CalendarInternal.isSameDay(d, today),
|
|
1593
1683
|
);
|
|
1594
1684
|
|
|
1595
|
-
if (
|
|
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) *
|
|
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
|
-
|
|
1985
|
-
|
|
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
|
|
2019
|
-
(
|
|
2125
|
+
const maxEventsInRow = Math.floor(
|
|
2126
|
+
(this.dayHeight - 4) / (MIN_EVENT_HEIGHT + 2),
|
|
2020
2127
|
);
|
|
2021
|
-
if (rowIndex >=
|
|
2128
|
+
if (rowIndex >= maxEventsInRow) continue;
|
|
2022
2129
|
|
|
2023
|
-
|
|
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
|
-
|
|
2158
|
+
startVisualPos.col * dayWidth +
|
|
2050
2159
|
columnLayout.column * columnWidth;
|
|
2051
2160
|
spanWidth = columnWidth;
|
|
2052
2161
|
} else {
|
|
2053
|
-
// Normal layout
|
|
2054
|
-
|
|
2055
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
-
|
|
2993
|
-
|
|
3084
|
+
deleteSelectedEvents(): void {
|
|
3085
|
+
const selectedEvents = this.internal.getSelectedEvents();
|
|
3086
|
+
if (selectedEvents.length === 0) return;
|
|
2994
3087
|
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
if (deletableEvents.length === 0) return;
|
|
3088
|
+
const deletableEvents = selectedEvents.filter((event) => !event.readOnly);
|
|
3089
|
+
if (deletableEvents.length === 0) return;
|
|
2998
3090
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3091
|
+
this.dispatchEvent(
|
|
3092
|
+
new CustomEvent("delete-events", {
|
|
3093
|
+
detail: { events: deletableEvents },
|
|
3094
|
+
bubbles: true,
|
|
3095
|
+
}),
|
|
3096
|
+
);
|
|
3001
3097
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
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
|
-
|
|
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 /
|
|
3592
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
3510
3593
|
|
|
3511
|
-
// Find
|
|
3512
|
-
const
|
|
3594
|
+
// Find column indices from X coordinates
|
|
3595
|
+
const startCol = Math.max(
|
|
3513
3596
|
0,
|
|
3514
|
-
Math.min(
|
|
3597
|
+
Math.min(this._columnsPerRow - 1, Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth)),
|
|
3515
3598
|
);
|
|
3516
|
-
const
|
|
3599
|
+
const endCol = Math.max(
|
|
3517
3600
|
0,
|
|
3518
|
-
Math.min(
|
|
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
|
-
|
|
3537
|
-
|
|
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 =
|
|
3640
|
+
const dayStartOffset = dayMinY - rowTop;
|
|
3550
3641
|
const startMinutes = Math.floor(
|
|
3551
|
-
(dayStartOffset /
|
|
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 =
|
|
3558
|
-
const endMinutes = Math.ceil((dayEndOffset /
|
|
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
|
|
3573
|
-
//
|
|
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
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
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
|
-
|
|
3584
|
-
|
|
3685
|
+
const rangeEnd = new Date(day);
|
|
3686
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
3585
3687
|
|
|
3586
|
-
|
|
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 /
|
|
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
|
-
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
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,
|
|
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,
|
|
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 /
|
|
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
|
|
4700
|
+
const col = Math.max(
|
|
4521
4701
|
0,
|
|
4522
|
-
Math.min(
|
|
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
|
|
4541
|
-
|
|
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 /
|
|
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 +
|
|
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
|
|
4583
|
-
const y = week.yOffset +
|
|
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">
|
|
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.
|
|
5021
|
-
?
|
|
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
|
-
|
|
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
|
-
<
|
|
5117
|
-
|
|
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">
|