@luckydye/calendar 1.3.0 → 1.3.2
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 +2198 -1539
- package/package.json +2 -2
- package/src/CalDAVConfig.ts +50 -1
- package/src/CalDAVSource.ts +52 -19
- package/src/CalendarIntegration.ts +2 -2
- package/src/CalendarInternal.ts +10 -4
- package/src/CalendarLayer.ts +28 -0
- package/src/CalendarView.ts +517 -1146
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +27 -1
- package/src/ICal.ts +7 -2
- package/src/InMemorySource.ts +6 -6
- package/src/IndexedDBStorage.ts +6 -0
- package/src/InhouseBookingSource.ts +2 -1
- package/src/Keybinds.ts +3 -18
- package/src/StatusBar.ts +11 -0
- package/src/Theme.ts +4 -4
- package/src/TimeseriesJson.ts +114 -0
- package/src/app.ts +199 -48
- package/src/layers/EventsLayer.ts +958 -0
- package/src/layers/GridLayer.ts +296 -0
- package/src/layers/TimeseriesHeatmapLayer.ts +132 -0
- package/src/lib.ts +1 -0
package/src/CalendarView.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
type Attendee,
|
|
4
4
|
type CalendarEvent,
|
|
5
5
|
CalendarInternal,
|
|
6
|
-
type EventSegment,
|
|
7
6
|
type WeekInfo,
|
|
8
7
|
NOTIFICATION_PRESETS,
|
|
9
8
|
} from "./CalendarInternal.js";
|
|
@@ -19,12 +18,16 @@ import { serializeEventsToICal } from "./ICal.js";
|
|
|
19
18
|
import { queueStatus } from "./StatusMessage.js";
|
|
20
19
|
import type { StatusBarData } from "./StatusBar.js";
|
|
21
20
|
import { IndexedDBStorage } from "./IndexedDBStorage.js";
|
|
21
|
+
import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
|
|
22
|
+
import { TIME_SCALE_DAY_HEIGHT, type CalendarLayer, type LayerContext } from "./CalendarLayer.js";
|
|
23
|
+
import { createGridLayer } from "./layers/GridLayer.js";
|
|
24
|
+
import { createEventsLayer, type EventRect, type EventsState } from "./layers/EventsLayer.js";
|
|
25
|
+
import { createTimeseriesHeatmapLayer } from "./layers/TimeseriesHeatmapLayer.js";
|
|
22
26
|
|
|
23
|
-
const MIN_DAY_HEIGHT =
|
|
27
|
+
const MIN_DAY_HEIGHT = 50;
|
|
24
28
|
const MAX_DAY_HEIGHT = 3000; // 1px per minute
|
|
25
29
|
const LEFT_GUTTER_WIDTH = 60;
|
|
26
30
|
const MINIMAP_WIDTH = 12;
|
|
27
|
-
const MIN_EVENT_HEIGHT = 20;
|
|
28
31
|
|
|
29
32
|
export class CalendarViewElement extends LitElement {
|
|
30
33
|
static styles = css`
|
|
@@ -257,7 +260,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
257
260
|
left: 60px;
|
|
258
261
|
right: 12px;
|
|
259
262
|
pointer-events: none;
|
|
260
|
-
z-index:
|
|
263
|
+
z-index: 101;
|
|
261
264
|
overflow: hidden;
|
|
262
265
|
}
|
|
263
266
|
|
|
@@ -298,7 +301,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
298
301
|
flex-direction: row;
|
|
299
302
|
backdrop-filter: blur(10px);
|
|
300
303
|
box-shadow: var(--shadow-overlay, 0 4px 12px rgba(0, 0, 0, 0.9));
|
|
301
|
-
z-index:
|
|
304
|
+
z-index: 102;
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
.event-detail-header {
|
|
@@ -677,6 +680,19 @@ export class CalendarViewElement extends LitElement {
|
|
|
677
680
|
text-align: left;
|
|
678
681
|
}
|
|
679
682
|
|
|
683
|
+
.description-actions {
|
|
684
|
+
display: flex;
|
|
685
|
+
gap: 12px;
|
|
686
|
+
align-items: center;
|
|
687
|
+
flex-wrap: wrap;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.description-actions .description-see-more {
|
|
691
|
+
display: inline-block;
|
|
692
|
+
margin-top: 8px;
|
|
693
|
+
padding: 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
680
696
|
.description-see-more:hover {
|
|
681
697
|
color: var(--accent-hover, rgb(120, 170, 255));
|
|
682
698
|
}
|
|
@@ -854,6 +870,14 @@ export class CalendarViewElement extends LitElement {
|
|
|
854
870
|
minimapBufferCanvas: HTMLCanvasElement | null = null;
|
|
855
871
|
minimapBufferCtx: CanvasRenderingContext2D | null = null;
|
|
856
872
|
stripePatternCanvas: HTMLCanvasElement | null = null;
|
|
873
|
+
layers: CalendarLayer[] = [];
|
|
874
|
+
eventsLayer: ReturnType<typeof createEventsLayer> | null = null;
|
|
875
|
+
heatmapEvents: CalendarEvent[] = [];
|
|
876
|
+
heatmapRange: { start: Date; end: Date } | null = null;
|
|
877
|
+
heatmapQueryKey: string | null = null;
|
|
878
|
+
heatmapQueryToken = 0;
|
|
879
|
+
timeseriesSourceIds: Set<string> = new Set();
|
|
880
|
+
timeseriesSourceCacheKey: string | null = null;
|
|
857
881
|
scrollContainer: HTMLElement | null = null;
|
|
858
882
|
scrollContent: HTMLElement | null = null;
|
|
859
883
|
resizeObserver: ResizeObserver | null = null;
|
|
@@ -875,9 +899,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
875
899
|
cursorPosition: { x: number; y: number } | null = null; // Mouse position in calendar coordinates
|
|
876
900
|
animationFrame: number | null = null;
|
|
877
901
|
isDraggingMinimap = false;
|
|
902
|
+
altKeyActive = false; // Tracks if alt/meta key is held (bypasses event interaction)
|
|
878
903
|
isFiltered = false; // Tracks if filter is currently active
|
|
879
904
|
timeUpdateInterval: ReturnType<typeof setInterval> | null = null;
|
|
880
905
|
isExtendingRange = false; // Prevents concurrent range extensions
|
|
906
|
+
boundaryCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
907
|
+
lastWheelTime = 0; // Timestamp of last wheel event, used to distinguish wheel vs scrollbar drag
|
|
881
908
|
isCreatingEvent = false;
|
|
882
909
|
eventCreationStart: { x: number; y: number } | null = null;
|
|
883
910
|
eventCreationEnd: { x: number; y: number } | null = null;
|
|
@@ -979,6 +1006,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
979
1006
|
height: number;
|
|
980
1007
|
} | null = null;
|
|
981
1008
|
isDescriptionExpanded = false;
|
|
1009
|
+
descriptionSummaryTargetKey: string | null = null;
|
|
1010
|
+
descriptionSummaryText = "";
|
|
1011
|
+
descriptionSummaryLoading = false;
|
|
1012
|
+
descriptionSummaryError: string | null = null;
|
|
1013
|
+
private requestedDescriptionSummaryKey: string | null = null;
|
|
982
1014
|
|
|
983
1015
|
notificationPopoverOpen = false;
|
|
984
1016
|
scheduledNotifications: any[] = [];
|
|
@@ -992,6 +1024,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
992
1024
|
currentTheme: ThemeName = loadThemePreference();
|
|
993
1025
|
|
|
994
1026
|
activeCalendarColor: string | null = null;
|
|
1027
|
+
activeCalendarId: string | null = null;
|
|
995
1028
|
|
|
996
1029
|
get filter() {
|
|
997
1030
|
return this.internal.getFilter();
|
|
@@ -1302,6 +1335,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
1302
1335
|
window.addEventListener("mousemove", this.onMouseMove);
|
|
1303
1336
|
window.addEventListener("mouseup", this.onMouseUp);
|
|
1304
1337
|
window.addEventListener("paste", this.onPaste);
|
|
1338
|
+
window.addEventListener("keydown", this.onKeyDown);
|
|
1339
|
+
window.addEventListener("keyup", this.onKeyUp);
|
|
1305
1340
|
}
|
|
1306
1341
|
|
|
1307
1342
|
disconnectedCallback() {
|
|
@@ -1310,6 +1345,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
1310
1345
|
window.removeEventListener("mousemove", this.onMouseMove);
|
|
1311
1346
|
window.removeEventListener("mouseup", this.onMouseUp);
|
|
1312
1347
|
window.removeEventListener("paste", this.onPaste);
|
|
1348
|
+
window.removeEventListener("keydown", this.onKeyDown);
|
|
1349
|
+
window.removeEventListener("keyup", this.onKeyUp);
|
|
1313
1350
|
|
|
1314
1351
|
if (this.scrollContainer) {
|
|
1315
1352
|
this.scrollContainer.removeEventListener(
|
|
@@ -1326,6 +1363,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1326
1363
|
if (this.timeUpdateInterval) {
|
|
1327
1364
|
clearInterval(this.timeUpdateInterval);
|
|
1328
1365
|
}
|
|
1366
|
+
if (this.boundaryCheckInterval) {
|
|
1367
|
+
clearInterval(this.boundaryCheckInterval);
|
|
1368
|
+
}
|
|
1329
1369
|
}
|
|
1330
1370
|
|
|
1331
1371
|
onScrollContainerMouseLeave = (): void => {
|
|
@@ -1338,6 +1378,97 @@ export class CalendarViewElement extends LitElement {
|
|
|
1338
1378
|
this.repositionEventDetailOverlay();
|
|
1339
1379
|
}
|
|
1340
1380
|
|
|
1381
|
+
clearDescriptionSummary(): void {
|
|
1382
|
+
if (this.requestedDescriptionSummaryKey) {
|
|
1383
|
+
this.dispatchEvent(
|
|
1384
|
+
new CustomEvent("cancel-description-summary", {
|
|
1385
|
+
detail: { key: this.requestedDescriptionSummaryKey },
|
|
1386
|
+
bubbles: true,
|
|
1387
|
+
composed: true,
|
|
1388
|
+
}),
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
this.requestedDescriptionSummaryKey = null;
|
|
1392
|
+
this.descriptionSummaryTargetKey = null;
|
|
1393
|
+
this.descriptionSummaryText = "";
|
|
1394
|
+
this.descriptionSummaryLoading = false;
|
|
1395
|
+
this.descriptionSummaryError = null;
|
|
1396
|
+
this.requestUpdate();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
private getDescriptionSummaryTargetKey(event: CalendarEvent): string {
|
|
1400
|
+
return `${event.id}:${sanitizeEventDescription(event.description ?? "")}`;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private requestDescriptionSummary(event: CalendarEvent): void {
|
|
1404
|
+
const description = sanitizeEventDescription(event.description ?? "").trim();
|
|
1405
|
+
if (description.length <= 200) return;
|
|
1406
|
+
|
|
1407
|
+
const targetKey = this.getDescriptionSummaryTargetKey(event);
|
|
1408
|
+
this.clearDescriptionSummary();
|
|
1409
|
+
this.requestedDescriptionSummaryKey = targetKey;
|
|
1410
|
+
this.descriptionSummaryTargetKey = targetKey;
|
|
1411
|
+
this.descriptionSummaryText = "";
|
|
1412
|
+
this.descriptionSummaryLoading = true;
|
|
1413
|
+
this.descriptionSummaryError = null;
|
|
1414
|
+
this.dispatchEvent(
|
|
1415
|
+
new CustomEvent("request-description-summary", {
|
|
1416
|
+
detail: {
|
|
1417
|
+
event,
|
|
1418
|
+
key: targetKey,
|
|
1419
|
+
description,
|
|
1420
|
+
},
|
|
1421
|
+
bubbles: true,
|
|
1422
|
+
composed: true,
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1425
|
+
this.requestUpdate();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
startDescriptionSummary(key: string): void {
|
|
1429
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1430
|
+
this.descriptionSummaryText = "";
|
|
1431
|
+
this.descriptionSummaryLoading = true;
|
|
1432
|
+
this.descriptionSummaryError = null;
|
|
1433
|
+
this.requestUpdate();
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
appendDescriptionSummaryChunk(key: string, chunk: string): void {
|
|
1437
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1438
|
+
if (!chunk) return;
|
|
1439
|
+
this.descriptionSummaryText += chunk;
|
|
1440
|
+
this.descriptionSummaryLoading = true;
|
|
1441
|
+
this.requestUpdate();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
finishDescriptionSummary(key: string, error: string | null = null): void {
|
|
1445
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1446
|
+
this.descriptionSummaryLoading = false;
|
|
1447
|
+
this.descriptionSummaryError = error;
|
|
1448
|
+
this.requestUpdate();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
failDescriptionSummary(key: string, message: string): void {
|
|
1452
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1453
|
+
this.descriptionSummaryLoading = false;
|
|
1454
|
+
this.descriptionSummaryError = message;
|
|
1455
|
+
if (!this.descriptionSummaryText && this.selectedEventForDetail?.description) {
|
|
1456
|
+
this.descriptionSummaryText = this.selectedEventForDetail.description.replaceAll(
|
|
1457
|
+
"\\n",
|
|
1458
|
+
"\n",
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
this.requestUpdate();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
cancelDescriptionSummary(key: string): void {
|
|
1465
|
+
if (key !== this.descriptionSummaryTargetKey) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
this.descriptionSummaryLoading = false;
|
|
1469
|
+
this.requestUpdate();
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1341
1472
|
repositionEventDetailOverlay(): void {
|
|
1342
1473
|
if (!this.selectedEventForDetail || !this.selectedEventRect) return;
|
|
1343
1474
|
|
|
@@ -1367,6 +1498,24 @@ export class CalendarViewElement extends LitElement {
|
|
|
1367
1498
|
this.minimapBufferCanvas = document.createElement("canvas");
|
|
1368
1499
|
this.minimapBufferCtx = this.minimapBufferCanvas.getContext("2d");
|
|
1369
1500
|
|
|
1501
|
+
const self = this;
|
|
1502
|
+
const eventsState: EventsState = {
|
|
1503
|
+
get events() { return self.events; },
|
|
1504
|
+
get hoveredEventId() { return self.hoveredEventId; },
|
|
1505
|
+
isEventSelected: (event) => self.internal.isEventSelected(event),
|
|
1506
|
+
shouldRenderEventWithStripes: (event) => self.shouldRenderEventWithStripes(event),
|
|
1507
|
+
getStripePatternCanvas: () => self.getStripePatternCanvas(),
|
|
1508
|
+
};
|
|
1509
|
+
const heatmapState = {
|
|
1510
|
+
get events() { return self.heatmapEvents; },
|
|
1511
|
+
};
|
|
1512
|
+
this.eventsLayer = createEventsLayer(eventsState);
|
|
1513
|
+
this.layers = [
|
|
1514
|
+
createGridLayer(),
|
|
1515
|
+
createTimeseriesHeatmapLayer(heatmapState),
|
|
1516
|
+
this.eventsLayer,
|
|
1517
|
+
];
|
|
1518
|
+
|
|
1370
1519
|
this.weeks = this.internal.generateWeeks();
|
|
1371
1520
|
|
|
1372
1521
|
// Restore zoom level from localStorage
|
|
@@ -1399,13 +1548,48 @@ export class CalendarViewElement extends LitElement {
|
|
|
1399
1548
|
this.requestUpdate();
|
|
1400
1549
|
}, 10000);
|
|
1401
1550
|
|
|
1551
|
+
// Extend range while stationary at the buffer boundary (no scroll events fire when idle)
|
|
1552
|
+
this.boundaryCheckInterval = setInterval(() => {
|
|
1553
|
+
this.checkAndExtendRange();
|
|
1554
|
+
}, 500);
|
|
1555
|
+
|
|
1402
1556
|
// Try to restore saved scroll position, otherwise scroll to today
|
|
1403
1557
|
this.loadScrollPosition();
|
|
1404
1558
|
|
|
1405
1559
|
let previousFilter = this.filter;
|
|
1406
1560
|
|
|
1407
1561
|
for await (const events of this.internal.events()) {
|
|
1408
|
-
|
|
1562
|
+
const timeseriesIds = this.loadTimeseriesSourceIds();
|
|
1563
|
+
let normalized = events;
|
|
1564
|
+
if (timeseriesIds.size > 0) {
|
|
1565
|
+
let needsClone = false;
|
|
1566
|
+
for (const event of events) {
|
|
1567
|
+
if (
|
|
1568
|
+
event.visualStyle !== "heatmap" &&
|
|
1569
|
+
event.sourceId &&
|
|
1570
|
+
timeseriesIds.has(event.sourceId)
|
|
1571
|
+
) {
|
|
1572
|
+
needsClone = true;
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (needsClone) {
|
|
1577
|
+
normalized = events.map((event) => {
|
|
1578
|
+
if (
|
|
1579
|
+
event.visualStyle === "heatmap" ||
|
|
1580
|
+
!event.sourceId ||
|
|
1581
|
+
!timeseriesIds.has(event.sourceId)
|
|
1582
|
+
) {
|
|
1583
|
+
return event;
|
|
1584
|
+
}
|
|
1585
|
+
return { ...event, visualStyle: "heatmap" };
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
this.events = normalized;
|
|
1590
|
+
if (this.heatmapRange) {
|
|
1591
|
+
void this.refreshHeatmapEvents(true);
|
|
1592
|
+
}
|
|
1409
1593
|
|
|
1410
1594
|
this.renderCanvas();
|
|
1411
1595
|
|
|
@@ -1548,6 +1732,101 @@ export class CalendarViewElement extends LitElement {
|
|
|
1548
1732
|
}
|
|
1549
1733
|
}
|
|
1550
1734
|
|
|
1735
|
+
resolveStyles(): Record<string, string> {
|
|
1736
|
+
const cs = getComputedStyle(this);
|
|
1737
|
+
const props = [
|
|
1738
|
+
"--bg-today",
|
|
1739
|
+
"--accent-current-time",
|
|
1740
|
+
"--grid-color",
|
|
1741
|
+
"--grid-color-strong",
|
|
1742
|
+
"--grid-color-hover",
|
|
1743
|
+
"--text-muted",
|
|
1744
|
+
"--text-primary",
|
|
1745
|
+
"--text-inverse",
|
|
1746
|
+
"--bg-primary",
|
|
1747
|
+
"--bg-elevated",
|
|
1748
|
+
];
|
|
1749
|
+
const styles: Record<string, string> = {};
|
|
1750
|
+
for (const prop of props) {
|
|
1751
|
+
styles[prop] = cs.getPropertyValue(prop).trim();
|
|
1752
|
+
}
|
|
1753
|
+
return styles;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
private loadTimeseriesSourceIds(): Set<string> {
|
|
1757
|
+
const saved = localStorage.getItem("caldav-sources") || "";
|
|
1758
|
+
if (saved && saved === this.timeseriesSourceCacheKey) {
|
|
1759
|
+
return this.timeseriesSourceIds;
|
|
1760
|
+
}
|
|
1761
|
+
const ids = new Set<string>();
|
|
1762
|
+
if (saved) {
|
|
1763
|
+
try {
|
|
1764
|
+
const sources = JSON.parse(saved) as Array<{ id?: string; type?: string }>;
|
|
1765
|
+
for (const source of sources) {
|
|
1766
|
+
if (source?.type === "timeseries-json" && source.id) {
|
|
1767
|
+
ids.add(source.id);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
} catch {
|
|
1771
|
+
// Ignore malformed storage
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
this.timeseriesSourceCacheKey = saved;
|
|
1775
|
+
this.timeseriesSourceIds = ids;
|
|
1776
|
+
return ids;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
private updateHeatmapRange(visibleWeeks: WeekInfo[]): void {
|
|
1780
|
+
if (visibleWeeks.length === 0) {
|
|
1781
|
+
this.heatmapRange = null;
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const firstWeek = visibleWeeks[0]!;
|
|
1785
|
+
const lastWeek = visibleWeeks[visibleWeeks.length - 1]!;
|
|
1786
|
+
const startDay = firstWeek.days[0];
|
|
1787
|
+
const endDay = lastWeek.days[6];
|
|
1788
|
+
if (!startDay || !endDay) {
|
|
1789
|
+
this.heatmapRange = null;
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const start = new Date(startDay);
|
|
1793
|
+
start.setHours(0, 0, 0, 0);
|
|
1794
|
+
const end = new Date(endDay);
|
|
1795
|
+
end.setHours(23, 59, 59, 999);
|
|
1796
|
+
this.heatmapRange = { start, end };
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private async refreshHeatmapEvents(force = false): Promise<void> {
|
|
1800
|
+
if (!this.heatmapRange) return;
|
|
1801
|
+
const { start, end } = this.heatmapRange;
|
|
1802
|
+
const timeseriesIds = this.loadTimeseriesSourceIds();
|
|
1803
|
+
const enabledKey = [...this.internal.enabledCalendars].join(",");
|
|
1804
|
+
const lockedKey = [...this.internal.lockedCalendars].join(",");
|
|
1805
|
+
const key = `${start.toISOString()}::${end.toISOString()}::${this.filter || ""}::${enabledKey}::${lockedKey}`;
|
|
1806
|
+
if (!force && key === this.heatmapQueryKey) return;
|
|
1807
|
+
this.heatmapQueryKey = key;
|
|
1808
|
+
|
|
1809
|
+
const token = ++this.heatmapQueryToken;
|
|
1810
|
+
try {
|
|
1811
|
+
let events: CalendarEvent[];
|
|
1812
|
+
if (this.internal.storage) {
|
|
1813
|
+
events = await this.internal.storage.queryEvents(start, end);
|
|
1814
|
+
} else {
|
|
1815
|
+
events = this.events;
|
|
1816
|
+
}
|
|
1817
|
+
if (token !== this.heatmapQueryToken) return;
|
|
1818
|
+
|
|
1819
|
+
const filtered = this.internal.filterEvents(events, this.filter);
|
|
1820
|
+
this.heatmapEvents = filtered.filter((event) => {
|
|
1821
|
+
if (event.visualStyle === "heatmap") return true;
|
|
1822
|
+
return event.sourceId ? timeseriesIds.has(event.sourceId) : false;
|
|
1823
|
+
});
|
|
1824
|
+
this.renderCanvas();
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
console.warn("Failed to refresh heatmap events:", error);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1551
1830
|
renderCanvas(): void {
|
|
1552
1831
|
if (!this.ctx || !this.canvas || !this.scrollContainer) return;
|
|
1553
1832
|
|
|
@@ -1561,292 +1840,48 @@ export class CalendarViewElement extends LitElement {
|
|
|
1561
1840
|
const scrollTop = this.scrollTop;
|
|
1562
1841
|
const gridWidth = width - LEFT_GUTTER_WIDTH;
|
|
1563
1842
|
const dayWidth = gridWidth / this._columnsPerRow;
|
|
1564
|
-
const today = new Date();
|
|
1565
1843
|
|
|
1566
|
-
// Find visible weeks
|
|
1567
1844
|
const visibleWeeks = this.weeks.filter(
|
|
1568
1845
|
(w) =>
|
|
1569
1846
|
w.height > 0 &&
|
|
1570
1847
|
w.yOffset + w.height > scrollTop &&
|
|
1571
1848
|
w.yOffset < scrollTop + height,
|
|
1572
1849
|
);
|
|
1850
|
+
this.updateHeatmapRange(visibleWeeks);
|
|
1851
|
+
this.refreshHeatmapEvents();
|
|
1852
|
+
|
|
1853
|
+
const lc: LayerContext = {
|
|
1854
|
+
ctx,
|
|
1855
|
+
width,
|
|
1856
|
+
height,
|
|
1857
|
+
scrollTop,
|
|
1858
|
+
dayWidth,
|
|
1859
|
+
dayHeight: this.dayHeight,
|
|
1860
|
+
leftGutterWidth: LEFT_GUTTER_WIDTH,
|
|
1861
|
+
columnsPerRow: this._columnsPerRow,
|
|
1862
|
+
rowsPerWeek: this.rowsPerWeek,
|
|
1863
|
+
visibleWeeks,
|
|
1864
|
+
allWeeks: this.weeks,
|
|
1865
|
+
fontFamily,
|
|
1866
|
+
styles: this.resolveStyles(),
|
|
1867
|
+
getDayVisualPosition: (dayIndex) => this.getDayVisualPosition(dayIndex),
|
|
1868
|
+
filter: this.filter,
|
|
1869
|
+
};
|
|
1573
1870
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
);
|
|
1580
|
-
if (todayIndex >= 0) {
|
|
1581
|
-
const { row, col } = this.getDayVisualPosition(todayIndex);
|
|
1582
|
-
const x = LEFT_GUTTER_WIDTH + col * dayWidth;
|
|
1583
|
-
const dayY = week.yOffset + row * this.dayHeight - scrollTop;
|
|
1584
|
-
const bgToday =
|
|
1585
|
-
getComputedStyle(this).getPropertyValue("--bg-today").trim() ||
|
|
1586
|
-
"rgba(255, 255, 255, 0.05)";
|
|
1587
|
-
ctx.fillStyle = bgToday;
|
|
1588
|
-
ctx.fillRect(x, dayY, dayWidth, this.dayHeight);
|
|
1589
|
-
|
|
1590
|
-
if (showTimeScale) {
|
|
1591
|
-
// Draw current time indicator line (zoomed in)
|
|
1592
|
-
const now = new Date();
|
|
1593
|
-
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
1594
|
-
const timeY = dayY + (currentMinutes / 1440) * this.dayHeight;
|
|
1595
|
-
if (timeY >= 0 && timeY <= height) {
|
|
1596
|
-
const accentTime =
|
|
1597
|
-
getComputedStyle(this)
|
|
1598
|
-
.getPropertyValue("--accent-current-time")
|
|
1599
|
-
.trim() || "rgba(255, 0, 0, 0.8)";
|
|
1600
|
-
ctx.strokeStyle = accentTime;
|
|
1601
|
-
ctx.lineWidth = 1;
|
|
1602
|
-
ctx.beginPath();
|
|
1603
|
-
ctx.moveTo(x, timeY);
|
|
1604
|
-
ctx.lineTo(x + dayWidth, timeY);
|
|
1605
|
-
ctx.stroke();
|
|
1606
|
-
ctx.lineWidth = 1;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
// Draw grid lines
|
|
1613
|
-
ctx.strokeStyle =
|
|
1614
|
-
getComputedStyle(this).getPropertyValue("--grid-color").trim() ||
|
|
1615
|
-
"rgba(255, 255, 255, 0.1)";
|
|
1616
|
-
ctx.lineWidth = 1;
|
|
1617
|
-
|
|
1618
|
-
// Vertical lines (day separators) - draw for each column
|
|
1619
|
-
for (let i = 1; i <= this._columnsPerRow; i++) {
|
|
1620
|
-
const x = LEFT_GUTTER_WIDTH + i * dayWidth;
|
|
1621
|
-
ctx.beginPath();
|
|
1622
|
-
ctx.moveTo(x, 0);
|
|
1623
|
-
ctx.lineTo(x, height);
|
|
1624
|
-
ctx.stroke();
|
|
1871
|
+
for (const layer of this.layers) {
|
|
1872
|
+
if (!layer.enabled) continue;
|
|
1873
|
+
ctx.save();
|
|
1874
|
+
layer.render(lc);
|
|
1875
|
+
ctx.restore();
|
|
1625
1876
|
}
|
|
1626
1877
|
|
|
1627
|
-
//
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
// Draw gap indicator if there are hidden weeks before this one
|
|
1634
|
-
if (this.filter && i > 0) {
|
|
1635
|
-
const prevWeek = visibleWeeks[i - 1];
|
|
1636
|
-
if (!prevWeek) continue;
|
|
1637
|
-
const prevWeekIndex = this.weeks.indexOf(prevWeek);
|
|
1638
|
-
const currentWeekIndex = this.weeks.indexOf(week);
|
|
1639
|
-
const hiddenWeeks = currentWeekIndex - prevWeekIndex - 1;
|
|
1640
|
-
|
|
1641
|
-
if (hiddenWeeks > 0) {
|
|
1642
|
-
// Draw gap indicator at the top of the current week
|
|
1643
|
-
const gapY = y;
|
|
1644
|
-
|
|
1645
|
-
// Draw dashed line
|
|
1646
|
-
const gridColorStrong =
|
|
1647
|
-
getComputedStyle(this)
|
|
1648
|
-
.getPropertyValue("--grid-color-strong")
|
|
1649
|
-
.trim() || "rgba(255, 255, 255, 0.3)";
|
|
1650
|
-
ctx.strokeStyle = gridColorStrong;
|
|
1651
|
-
ctx.lineWidth = 1;
|
|
1652
|
-
ctx.setLineDash([4, 4]);
|
|
1653
|
-
ctx.beginPath();
|
|
1654
|
-
ctx.moveTo(LEFT_GUTTER_WIDTH, gapY);
|
|
1655
|
-
ctx.lineTo(width, gapY);
|
|
1656
|
-
ctx.stroke();
|
|
1657
|
-
ctx.setLineDash([]);
|
|
1658
|
-
|
|
1659
|
-
// Draw ellipsis in the center
|
|
1660
|
-
const textMuted =
|
|
1661
|
-
getComputedStyle(this).getPropertyValue("--text-muted").trim() ||
|
|
1662
|
-
"rgba(255, 255, 255, 0.4)";
|
|
1663
|
-
ctx.fillStyle = textMuted;
|
|
1664
|
-
ctx.textAlign = "center";
|
|
1665
|
-
const ellipsisText = `⋯ ${hiddenWeeks} week${
|
|
1666
|
-
hiddenWeeks > 1 ? "s" : ""
|
|
1667
|
-
}`;
|
|
1668
|
-
|
|
1669
|
-
// Draw background pill for the ellipsis
|
|
1670
|
-
const textWidth = ctx.measureText(ellipsisText).width;
|
|
1671
|
-
const pillPadding = 8;
|
|
1672
|
-
const pillX =
|
|
1673
|
-
(LEFT_GUTTER_WIDTH + width) / 2 - textWidth / 2 - pillPadding;
|
|
1674
|
-
const pillY = gapY - 8;
|
|
1675
|
-
const pillWidth = textWidth + pillPadding * 2;
|
|
1676
|
-
const pillHeight = 16;
|
|
1677
|
-
|
|
1678
|
-
const bgPrimary =
|
|
1679
|
-
getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
|
|
1680
|
-
"rgba(30, 30, 30, 0.9)";
|
|
1681
|
-
ctx.fillStyle = bgPrimary;
|
|
1682
|
-
ctx.beginPath();
|
|
1683
|
-
ctx.roundRect(pillX, pillY, pillWidth, pillHeight, 8);
|
|
1684
|
-
ctx.fill();
|
|
1685
|
-
|
|
1686
|
-
ctx.fillStyle = textMuted;
|
|
1687
|
-
ctx.fillText(ellipsisText, (LEFT_GUTTER_WIDTH + width) / 2, gapY + 4);
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
// Week separator line
|
|
1692
|
-
ctx.strokeStyle =
|
|
1693
|
-
getComputedStyle(this).getPropertyValue("--grid-color").trim() ||
|
|
1694
|
-
"rgba(255, 255, 255, 0.1)";
|
|
1695
|
-
ctx.beginPath();
|
|
1696
|
-
ctx.moveTo(LEFT_GUTTER_WIDTH, y);
|
|
1697
|
-
ctx.lineTo(width, y);
|
|
1698
|
-
ctx.stroke();
|
|
1699
|
-
|
|
1700
|
-
// Draw horizontal lines between visual rows within this week
|
|
1701
|
-
for (let row = 1; row < this.rowsPerWeek; row++) {
|
|
1702
|
-
const rowY = y + row * this.dayHeight;
|
|
1703
|
-
if (rowY >= 0 && rowY <= height) {
|
|
1704
|
-
ctx.beginPath();
|
|
1705
|
-
ctx.moveTo(LEFT_GUTTER_WIDTH, rowY);
|
|
1706
|
-
ctx.lineTo(width, rowY);
|
|
1707
|
-
ctx.stroke();
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
// Left gutter: week number and time scale
|
|
1712
|
-
const hourLabelOpacity = Math.max(
|
|
1713
|
-
0,
|
|
1714
|
-
Math.min(1, (this.dayHeight - 300) / 300),
|
|
1715
|
-
);
|
|
1716
|
-
|
|
1717
|
-
ctx.font = `500 11px ${fontFamily}`;
|
|
1718
|
-
ctx.textBaseline = "bottom";
|
|
1719
|
-
ctx.textAlign = "right";
|
|
1720
|
-
|
|
1721
|
-
// Draw hourly lines and labels for each visual row
|
|
1722
|
-
const gridColor =
|
|
1723
|
-
getComputedStyle(this).getPropertyValue("--grid-color").trim() ||
|
|
1724
|
-
"rgba(255, 255, 255, 0.1)";
|
|
1725
|
-
const textMuted =
|
|
1726
|
-
getComputedStyle(this).getPropertyValue("--text-muted").trim() ||
|
|
1727
|
-
"rgba(255, 255, 255, 0.4)";
|
|
1728
|
-
ctx.strokeStyle = gridColor.replace(
|
|
1729
|
-
/[\d.]+\)$/,
|
|
1730
|
-
`${0.05 * hourLabelOpacity})`,
|
|
1731
|
-
);
|
|
1732
|
-
|
|
1733
|
-
// Calculate opacity for hour labels based on zoom level
|
|
1734
|
-
// Fade in as we zoom in from 200px to 400px
|
|
1735
|
-
ctx.fillStyle = textMuted.replace(
|
|
1736
|
-
/[\d.]+\)$/,
|
|
1737
|
-
`${0.4 * hourLabelOpacity})`,
|
|
1738
|
-
);
|
|
1739
|
-
|
|
1740
|
-
for (let row = 0; row < this.rowsPerWeek; row++) {
|
|
1741
|
-
const rowY = y + row * this.dayHeight;
|
|
1742
|
-
for (let hour = 0; hour < 24; hour++) {
|
|
1743
|
-
const hourY = rowY + (hour / 24) * this.dayHeight;
|
|
1744
|
-
if (hourY >= 0 && hourY <= height) {
|
|
1745
|
-
// Hour line
|
|
1746
|
-
ctx.beginPath();
|
|
1747
|
-
ctx.moveTo(LEFT_GUTTER_WIDTH, hourY);
|
|
1748
|
-
ctx.lineTo(width, hourY);
|
|
1749
|
-
ctx.stroke();
|
|
1750
|
-
|
|
1751
|
-
// Hour label (only draw if opacity is significant)
|
|
1752
|
-
if (hourLabelOpacity > 0.1) {
|
|
1753
|
-
const label = `${hour.toString().padStart(2, "0")}:00`;
|
|
1754
|
-
ctx.fillText(label, 48, hourY + 4);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// Draw current time indicator in left gutter (only when timescale is visible and today is in this week)
|
|
1761
|
-
if (hourLabelOpacity > 0.1) {
|
|
1762
|
-
const today = new Date();
|
|
1763
|
-
const todayIndex = week.days.findIndex((d) =>
|
|
1764
|
-
CalendarInternal.isSameDay(d, today),
|
|
1765
|
-
);
|
|
1766
|
-
|
|
1767
|
-
if (todayIndex >= 0) {
|
|
1768
|
-
const { row } = this.getDayVisualPosition(todayIndex);
|
|
1769
|
-
const currentMinutes = today.getHours() * 60 + today.getMinutes();
|
|
1770
|
-
const timeY =
|
|
1771
|
-
y + row * this.dayHeight + (currentMinutes / 1440) * this.dayHeight;
|
|
1772
|
-
|
|
1773
|
-
if (timeY >= 0 && timeY <= height) {
|
|
1774
|
-
const hours = today.getHours().toString().padStart(2, "0");
|
|
1775
|
-
const minutes = today.getMinutes().toString().padStart(2, "0");
|
|
1776
|
-
const timeText = `${hours}:${minutes}`;
|
|
1777
|
-
|
|
1778
|
-
ctx.save();
|
|
1779
|
-
ctx.textAlign = "right";
|
|
1780
|
-
ctx.textBaseline = "middle";
|
|
1781
|
-
|
|
1782
|
-
const textWidth = ctx.measureText(timeText).width;
|
|
1783
|
-
const bgPaddingX = 6;
|
|
1784
|
-
const bgPaddingY = 3;
|
|
1785
|
-
const textX = 48;
|
|
1786
|
-
const textY = timeY;
|
|
1787
|
-
|
|
1788
|
-
// Draw background pill
|
|
1789
|
-
const bgElevated =
|
|
1790
|
-
getComputedStyle(this).getPropertyValue("--bg-elevated").trim() ||
|
|
1791
|
-
"rgba(0, 0, 0, 0.7)";
|
|
1792
|
-
ctx.fillStyle = bgElevated;
|
|
1793
|
-
ctx.beginPath();
|
|
1794
|
-
ctx.roundRect(
|
|
1795
|
-
textX - textWidth - bgPaddingX,
|
|
1796
|
-
textY - 8,
|
|
1797
|
-
textWidth + bgPaddingX * 2,
|
|
1798
|
-
16,
|
|
1799
|
-
4,
|
|
1800
|
-
);
|
|
1801
|
-
ctx.fill();
|
|
1802
|
-
|
|
1803
|
-
// Draw current time text in white
|
|
1804
|
-
ctx.fillStyle =
|
|
1805
|
-
getComputedStyle(this)
|
|
1806
|
-
.getPropertyValue("--text-primary")
|
|
1807
|
-
.trim() || "rgba(255, 255, 255, 1)";
|
|
1808
|
-
ctx.fillText(timeText, textX, textY);
|
|
1809
|
-
ctx.restore();
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
// Draw week number - sticky within visible portion of week
|
|
1815
|
-
// Fade out week numbers as we zoom in from 150px to 200px
|
|
1816
|
-
const weekNumberOpacity = Math.max(
|
|
1817
|
-
0,
|
|
1818
|
-
Math.min(1, 1 - (this.dayHeight - 300) / 50),
|
|
1819
|
-
);
|
|
1820
|
-
const textMutedForWeek =
|
|
1821
|
-
getComputedStyle(this).getPropertyValue("--text-muted").trim() ||
|
|
1822
|
-
"rgba(255, 255, 255, 0.4)";
|
|
1823
|
-
ctx.fillStyle = textMutedForWeek.replace(
|
|
1824
|
-
/[\d.]+\)$/,
|
|
1825
|
-
`${0.4 * weekNumberOpacity})`,
|
|
1826
|
-
);
|
|
1827
|
-
ctx.textAlign = "center";
|
|
1828
|
-
const label = `W${week.weekNumber}`;
|
|
1829
|
-
const weekTop = y;
|
|
1830
|
-
const weekBottom = y + week.height;
|
|
1831
|
-
// Clamp label to be visible: at least 14px from top of viewport,
|
|
1832
|
-
// but within the week's bounds
|
|
1833
|
-
const labelY = Math.max(
|
|
1834
|
-
14,
|
|
1835
|
-
Math.min(weekTop + week.height / 2 + 4, weekBottom - 4),
|
|
1836
|
-
);
|
|
1837
|
-
// Only draw if the label position is within the week's visible area and viewport and opacity is significant
|
|
1838
|
-
if (
|
|
1839
|
-
labelY >= Math.max(0, weekTop + 4) &&
|
|
1840
|
-
labelY <= Math.min(height, weekBottom) &&
|
|
1841
|
-
weekNumberOpacity > 0.1
|
|
1842
|
-
) {
|
|
1843
|
-
ctx.fillText(label, 30, labelY);
|
|
1844
|
-
}
|
|
1878
|
+
// Copy event rects from events layer for hit-testing
|
|
1879
|
+
if (this.eventsLayer) {
|
|
1880
|
+
this.eventRects = this.eventsLayer.eventRects;
|
|
1881
|
+
} else {
|
|
1882
|
+
this.eventRects = [];
|
|
1845
1883
|
}
|
|
1846
1884
|
|
|
1847
|
-
// Render events on canvas
|
|
1848
|
-
this.renderEventsOnCanvas(ctx, scrollTop, height, dayWidth, visibleWeeks);
|
|
1849
|
-
|
|
1850
1885
|
this.renderDateLabels();
|
|
1851
1886
|
|
|
1852
1887
|
// Draw sticky weekday labels at top of viewport
|
|
@@ -1862,808 +1897,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
1862
1897
|
this.renderMinimap();
|
|
1863
1898
|
}
|
|
1864
1899
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
visibleWeeks: WeekInfo[],
|
|
1871
|
-
): void {
|
|
1872
|
-
const events = this.events;
|
|
1873
|
-
const fontFamily = getComputedStyle(this).fontFamily;
|
|
1874
|
-
const viewportBottom = scrollTop + height;
|
|
1875
|
-
const showTimeScale = this.dayHeight >= 300;
|
|
1876
|
-
|
|
1877
|
-
// Clear event rects for new render
|
|
1878
|
-
this.eventRects = [];
|
|
1879
|
-
|
|
1880
|
-
if (visibleWeeks.length === 0) return;
|
|
1881
|
-
|
|
1882
|
-
// Compute visible date range
|
|
1883
|
-
const firstVisibleWeek = visibleWeeks[0]!;
|
|
1884
|
-
const lastVisibleWeek = visibleWeeks[visibleWeeks.length - 1]!;
|
|
1885
|
-
const firstVisibleDay = firstVisibleWeek.days[0]!;
|
|
1886
|
-
const lastVisibleDay = lastVisibleWeek.days[6]!;
|
|
1887
|
-
const visibleStartTime = firstVisibleDay.getTime();
|
|
1888
|
-
const visibleEndTime = lastVisibleDay.getTime() + 86400000 - 1;
|
|
1889
|
-
|
|
1890
|
-
// Track occupied row slots per day-column for stacking
|
|
1891
|
-
const dayOccupiedRows = new Map<string, Set<number>>();
|
|
1892
|
-
const eventRowIndex = new Map<string, number>();
|
|
1893
|
-
|
|
1894
|
-
const segments: EventSegment[] = [];
|
|
1895
|
-
|
|
1896
|
-
// Build segments - separate all-day and timed events
|
|
1897
|
-
const allDaySegments: EventSegment[] = [];
|
|
1898
|
-
const timedSegments: EventSegment[] = [];
|
|
1899
|
-
|
|
1900
|
-
for (const event of events) {
|
|
1901
|
-
const eventStartTime = event.start.getTime();
|
|
1902
|
-
const eventEndTime = event.end.getTime();
|
|
1903
|
-
|
|
1904
|
-
if (eventEndTime < visibleStartTime || eventStartTime > visibleEndTime)
|
|
1905
|
-
continue;
|
|
1906
|
-
|
|
1907
|
-
// Only iterate through visible weeks
|
|
1908
|
-
const eventWeeks: { weekIndex: number; week: WeekInfo }[] = [];
|
|
1909
|
-
|
|
1910
|
-
for (const week of visibleWeeks) {
|
|
1911
|
-
const weekStartDay = week.days[0];
|
|
1912
|
-
const weekEndDay = week.days[6];
|
|
1913
|
-
if (!weekStartDay || !weekEndDay) continue;
|
|
1914
|
-
|
|
1915
|
-
const weekStart = weekStartDay.getTime();
|
|
1916
|
-
const weekEnd = weekEndDay.getTime() + 86399999;
|
|
1917
|
-
|
|
1918
|
-
if (eventEndTime >= weekStart && eventStartTime <= weekEnd) {
|
|
1919
|
-
const weekIndex = this.weeks.indexOf(week);
|
|
1920
|
-
eventWeeks.push({ weekIndex, week });
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
const totalWeeks = eventWeeks.length;
|
|
1925
|
-
const isAllDay = event.isAllDay === true;
|
|
1926
|
-
|
|
1927
|
-
for (let i = 0; i < eventWeeks.length; i++) {
|
|
1928
|
-
const { weekIndex, week } = eventWeeks[i]!;
|
|
1929
|
-
|
|
1930
|
-
const isStart = i === 0;
|
|
1931
|
-
const isEnd = i === eventWeeks.length - 1;
|
|
1932
|
-
|
|
1933
|
-
let startDayIndex = 0;
|
|
1934
|
-
let endDayIndex = 6;
|
|
1935
|
-
|
|
1936
|
-
if (isStart) {
|
|
1937
|
-
startDayIndex = getDayIndexInWeek(week, event.start);
|
|
1938
|
-
}
|
|
1939
|
-
if (isEnd) {
|
|
1940
|
-
// For all-day events, the end date is exclusive (e.g., Monday event ends Tuesday 00:00)
|
|
1941
|
-
// Subtract 1ms to get the actual last day
|
|
1942
|
-
const effectiveEnd = isAllDay
|
|
1943
|
-
? new Date(event.end.getTime() - 1)
|
|
1944
|
-
: event.end;
|
|
1945
|
-
endDayIndex = getDayIndexInWeek(week, effectiveEnd);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
const segment = {
|
|
1949
|
-
event,
|
|
1950
|
-
weekIndex,
|
|
1951
|
-
week,
|
|
1952
|
-
startDayIndex,
|
|
1953
|
-
endDayIndex,
|
|
1954
|
-
isStart,
|
|
1955
|
-
isEnd,
|
|
1956
|
-
totalWeeks,
|
|
1957
|
-
};
|
|
1958
|
-
|
|
1959
|
-
if (isAllDay) {
|
|
1960
|
-
allDaySegments.push(segment);
|
|
1961
|
-
} else {
|
|
1962
|
-
timedSegments.push(segment);
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
// In timescale mode, split multi-day timed segments into per-day sub-segments
|
|
1968
|
-
// so each day renders its own block with correct time positions (like renderVirtualEvent).
|
|
1969
|
-
const renderedTimedSegments = showTimeScale
|
|
1970
|
-
? timedSegments.flatMap((seg) => {
|
|
1971
|
-
if (seg.startDayIndex === seg.endDayIndex) return [seg];
|
|
1972
|
-
const result = [];
|
|
1973
|
-
for (let d = seg.startDayIndex; d <= seg.endDayIndex; d++) {
|
|
1974
|
-
result.push({
|
|
1975
|
-
...seg,
|
|
1976
|
-
startDayIndex: d,
|
|
1977
|
-
endDayIndex: d,
|
|
1978
|
-
isStart: d === seg.startDayIndex && seg.isStart,
|
|
1979
|
-
isEnd: d === seg.endDayIndex && seg.isEnd,
|
|
1980
|
-
totalWeeks: 1,
|
|
1981
|
-
});
|
|
1982
|
-
}
|
|
1983
|
-
return result;
|
|
1984
|
-
})
|
|
1985
|
-
: timedSegments;
|
|
1986
|
-
|
|
1987
|
-
// Combine segments with all-day events first (so they get top rows)
|
|
1988
|
-
segments.push(...allDaySegments, ...renderedTimedSegments);
|
|
1989
|
-
|
|
1990
|
-
// Calculate columns for overlapping events in time scale mode
|
|
1991
|
-
interface EventLayout {
|
|
1992
|
-
segment: EventSegment;
|
|
1993
|
-
yStart: number;
|
|
1994
|
-
yEnd: number;
|
|
1995
|
-
column: number;
|
|
1996
|
-
totalColumns: number;
|
|
1997
|
-
}
|
|
1998
|
-
const eventLayouts: EventLayout[] = [];
|
|
1999
|
-
|
|
2000
|
-
// Group segments by week and day for overlap detection
|
|
2001
|
-
const segmentsByWeekDay = new Map<string, EventSegment[]>();
|
|
2002
|
-
for (const segment of segments) {
|
|
2003
|
-
const allDay = segment.event.isAllDay === true;
|
|
2004
|
-
|
|
2005
|
-
// Only do column layout for time-scaled non-all-day events
|
|
2006
|
-
if (showTimeScale && !allDay) {
|
|
2007
|
-
// For multi-day events in time scale, each day gets its own column calculation
|
|
2008
|
-
for (
|
|
2009
|
-
let dayIdx = segment.startDayIndex;
|
|
2010
|
-
dayIdx <= segment.endDayIndex;
|
|
2011
|
-
dayIdx++
|
|
2012
|
-
) {
|
|
2013
|
-
const key = `${segment.weekIndex}-${dayIdx}`;
|
|
2014
|
-
if (!segmentsByWeekDay.has(key)) {
|
|
2015
|
-
segmentsByWeekDay.set(key, []);
|
|
2016
|
-
}
|
|
2017
|
-
segmentsByWeekDay.get(key)!.push(segment);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
// Calculate column assignments for each day's overlapping events
|
|
2023
|
-
const segmentColumns = new Map<
|
|
2024
|
-
string,
|
|
2025
|
-
{ column: number; totalColumns: number }
|
|
2026
|
-
>();
|
|
2027
|
-
|
|
2028
|
-
for (const [dayKey, daySegments] of segmentsByWeekDay) {
|
|
2029
|
-
const [weekIndexStr, dayIndexStr] = dayKey.split("-");
|
|
2030
|
-
const dayIndex = parseInt(dayIndexStr!);
|
|
2031
|
-
const weekIndex = parseInt(weekIndexStr!);
|
|
2032
|
-
|
|
2033
|
-
// Calculate time ranges for each segment in this day
|
|
2034
|
-
interface SegmentTime {
|
|
2035
|
-
segment: EventSegment;
|
|
2036
|
-
startMinutes: number;
|
|
2037
|
-
endMinutes: number;
|
|
2038
|
-
}
|
|
2039
|
-
const segmentTimes: SegmentTime[] = [];
|
|
2040
|
-
|
|
2041
|
-
for (const seg of daySegments) {
|
|
2042
|
-
const week = this.weeks[weekIndex];
|
|
2043
|
-
if (!week) continue;
|
|
2044
|
-
|
|
2045
|
-
const dayStartTime = new Date(week.days[dayIndex]!).setHours(
|
|
2046
|
-
0,
|
|
2047
|
-
0,
|
|
2048
|
-
0,
|
|
2049
|
-
0,
|
|
2050
|
-
);
|
|
2051
|
-
const dayEndTime = new Date(week.days[dayIndex]!).setHours(
|
|
2052
|
-
23,
|
|
2053
|
-
59,
|
|
2054
|
-
59,
|
|
2055
|
-
999,
|
|
2056
|
-
);
|
|
2057
|
-
const eventStartTime = seg.event.start.getTime();
|
|
2058
|
-
const eventEndTime = seg.event.end.getTime() - 60000; // Subtract 1 minute for rendering
|
|
2059
|
-
|
|
2060
|
-
const effectiveStartTime = Math.max(eventStartTime, dayStartTime);
|
|
2061
|
-
const effectiveEndTime = Math.min(eventEndTime, dayEndTime);
|
|
2062
|
-
|
|
2063
|
-
const effectiveStart = new Date(effectiveStartTime);
|
|
2064
|
-
const effectiveEnd = new Date(effectiveEndTime);
|
|
2065
|
-
|
|
2066
|
-
const startMinutes =
|
|
2067
|
-
effectiveStart.getHours() * 60 + effectiveStart.getMinutes();
|
|
2068
|
-
const endMinutes =
|
|
2069
|
-
effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
|
|
2070
|
-
|
|
2071
|
-
segmentTimes.push({ segment: seg, startMinutes, endMinutes });
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
// Sort by start time, then by duration (longer first)
|
|
2075
|
-
segmentTimes.sort((a, b) => {
|
|
2076
|
-
if (a.startMinutes !== b.startMinutes)
|
|
2077
|
-
return a.startMinutes - b.startMinutes;
|
|
2078
|
-
return b.endMinutes - b.startMinutes - (a.endMinutes - a.startMinutes);
|
|
2079
|
-
});
|
|
2080
|
-
|
|
2081
|
-
// Assign columns using a greedy algorithm
|
|
2082
|
-
const columns: { endMinutes: number }[] = [];
|
|
2083
|
-
|
|
2084
|
-
for (const st of segmentTimes) {
|
|
2085
|
-
// Find the first column where this event fits
|
|
2086
|
-
let columnIndex = 0;
|
|
2087
|
-
for (; columnIndex < columns.length; columnIndex++) {
|
|
2088
|
-
if (columns[columnIndex]!.endMinutes <= st.startMinutes) {
|
|
2089
|
-
// This column is free
|
|
2090
|
-
break;
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// If no existing column works, create a new one
|
|
2095
|
-
if (columnIndex === columns.length) {
|
|
2096
|
-
columns.push({ endMinutes: st.endMinutes });
|
|
2097
|
-
} else {
|
|
2098
|
-
columns[columnIndex]!.endMinutes = st.endMinutes;
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
const segmentKey = `${st.segment.weekIndex}-${st.segment.event.id}-${dayIndex}`;
|
|
2102
|
-
segmentColumns.set(segmentKey, {
|
|
2103
|
-
column: columnIndex,
|
|
2104
|
-
totalColumns: columns.length,
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// Update total columns for all segments in this day
|
|
2109
|
-
for (const st of segmentTimes) {
|
|
2110
|
-
const segmentKey = `${st.segment.weekIndex}-${st.segment.event.id}-${dayIndex}`;
|
|
2111
|
-
const layout = segmentColumns.get(segmentKey);
|
|
2112
|
-
if (layout) {
|
|
2113
|
-
layout.totalColumns = columns.length;
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
// Render segments
|
|
2119
|
-
for (const segment of segments) {
|
|
2120
|
-
const {
|
|
2121
|
-
event,
|
|
2122
|
-
week,
|
|
2123
|
-
weekIndex,
|
|
2124
|
-
startDayIndex,
|
|
2125
|
-
endDayIndex,
|
|
2126
|
-
isStart,
|
|
2127
|
-
isEnd,
|
|
2128
|
-
totalWeeks,
|
|
2129
|
-
} = segment;
|
|
2130
|
-
const weekYOffset = week.yOffset;
|
|
2131
|
-
|
|
2132
|
-
const allDay = event.isAllDay === true;
|
|
2133
|
-
|
|
2134
|
-
// Get visual position for the start day
|
|
2135
|
-
const startVisualPos = this.getDayVisualPosition(startDayIndex);
|
|
2136
|
-
const endVisualPos = this.getDayVisualPosition(endDayIndex);
|
|
2137
|
-
|
|
2138
|
-
// Skip segments that span multiple visual rows for now (would need to be split)
|
|
2139
|
-
// For simple case, render each day on its visual row
|
|
2140
|
-
if (startVisualPos.row !== endVisualPos.row) {
|
|
2141
|
-
// Event spans visual rows - for now, only render the start portion
|
|
2142
|
-
// TODO: Split into multiple segments across visual rows
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
let yStart: number;
|
|
2146
|
-
let yEnd: number;
|
|
2147
|
-
|
|
2148
|
-
if (showTimeScale && !allDay) {
|
|
2149
|
-
const dayStartTime = new Date(week.days[startDayIndex]!).setHours(
|
|
2150
|
-
0,
|
|
2151
|
-
0,
|
|
2152
|
-
0,
|
|
2153
|
-
0,
|
|
2154
|
-
);
|
|
2155
|
-
const dayEndTime = new Date(week.days[endDayIndex]!).setHours(
|
|
2156
|
-
23,
|
|
2157
|
-
59,
|
|
2158
|
-
59,
|
|
2159
|
-
999,
|
|
2160
|
-
);
|
|
2161
|
-
const eventStartTime = event.start.getTime();
|
|
2162
|
-
const eventEndTime = event.end.getTime() - 60000; // Subtract 1 minute for rendering
|
|
2163
|
-
|
|
2164
|
-
const effectiveStartTime = Math.max(eventStartTime, dayStartTime);
|
|
2165
|
-
const effectiveEndTime = Math.min(eventEndTime, dayEndTime);
|
|
2166
|
-
|
|
2167
|
-
const effectiveStart = new Date(effectiveStartTime);
|
|
2168
|
-
const effectiveEnd = new Date(effectiveEndTime);
|
|
2169
|
-
|
|
2170
|
-
const startMinutes =
|
|
2171
|
-
effectiveStart.getHours() * 60 + effectiveStart.getMinutes();
|
|
2172
|
-
const endMinutes =
|
|
2173
|
-
effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
|
|
2174
|
-
|
|
2175
|
-
// Position within the visual row
|
|
2176
|
-
const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
|
|
2177
|
-
yStart = visualRowY + (startMinutes / 1440) * this.dayHeight;
|
|
2178
|
-
yEnd = visualRowY + (endMinutes / 1440) * this.dayHeight;
|
|
2179
|
-
} else {
|
|
2180
|
-
const eventKey = `${weekIndex}-${event.id}`;
|
|
2181
|
-
let rowIndex = eventRowIndex.get(eventKey);
|
|
2182
|
-
|
|
2183
|
-
if (rowIndex === undefined) {
|
|
2184
|
-
rowIndex = 0;
|
|
2185
|
-
while (true) {
|
|
2186
|
-
let rowFree = true;
|
|
2187
|
-
for (let d = startDayIndex; d <= endDayIndex; d++) {
|
|
2188
|
-
const dayKey = `${weekIndex}-${d}`;
|
|
2189
|
-
const occupied = dayOccupiedRows.get(dayKey);
|
|
2190
|
-
if (occupied?.has(rowIndex)) {
|
|
2191
|
-
rowFree = false;
|
|
2192
|
-
break;
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
if (rowFree) break;
|
|
2196
|
-
rowIndex++;
|
|
2197
|
-
}
|
|
2198
|
-
eventRowIndex.set(eventKey, rowIndex);
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
for (let d = startDayIndex; d <= endDayIndex; d++) {
|
|
2202
|
-
const dayKey = `${weekIndex}-${d}`;
|
|
2203
|
-
let occupied = dayOccupiedRows.get(dayKey);
|
|
2204
|
-
if (!occupied) {
|
|
2205
|
-
occupied = new Set();
|
|
2206
|
-
dayOccupiedRows.set(dayKey, occupied);
|
|
2207
|
-
}
|
|
2208
|
-
occupied.add(rowIndex);
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
const maxEventsInRow = Math.floor(
|
|
2212
|
-
(this.dayHeight - 4) / (MIN_EVENT_HEIGHT + 2),
|
|
2213
|
-
);
|
|
2214
|
-
if (rowIndex >= maxEventsInRow) continue;
|
|
2215
|
-
|
|
2216
|
-
// Position within the visual row
|
|
2217
|
-
const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
|
|
2218
|
-
yStart = visualRowY + 4 + rowIndex * (MIN_EVENT_HEIGHT + 2);
|
|
2219
|
-
yEnd = yStart + MIN_EVENT_HEIGHT;
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
const eventHeight = Math.max(
|
|
2223
|
-
showTimeScale ? 4 : MIN_EVENT_HEIGHT,
|
|
2224
|
-
yEnd - yStart,
|
|
2225
|
-
);
|
|
2226
|
-
|
|
2227
|
-
// Calculate horizontal position with column layout for time-scaled events
|
|
2228
|
-
let x: number;
|
|
2229
|
-
let spanWidth: number;
|
|
2230
|
-
|
|
2231
|
-
const segmentKey = `${weekIndex}-${event.id}-${startDayIndex}`;
|
|
2232
|
-
const columnLayout = segmentColumns.get(segmentKey);
|
|
2233
|
-
|
|
2234
|
-
if (
|
|
2235
|
-
showTimeScale &&
|
|
2236
|
-
!allDay &&
|
|
2237
|
-
columnLayout &&
|
|
2238
|
-
columnLayout.totalColumns > 1
|
|
2239
|
-
) {
|
|
2240
|
-
// This event has parallel events - divide the day width
|
|
2241
|
-
const columnWidth = dayWidth / columnLayout.totalColumns;
|
|
2242
|
-
x =
|
|
2243
|
-
LEFT_GUTTER_WIDTH +
|
|
2244
|
-
startVisualPos.col * dayWidth +
|
|
2245
|
-
columnLayout.column * columnWidth;
|
|
2246
|
-
spanWidth = columnWidth;
|
|
2247
|
-
} else {
|
|
2248
|
-
// Normal layout - use visual column position
|
|
2249
|
-
// For multi-day events on same visual row, calculate span
|
|
2250
|
-
const colSpan = endVisualPos.col - startVisualPos.col + 1;
|
|
2251
|
-
x = LEFT_GUTTER_WIDTH + startVisualPos.col * dayWidth;
|
|
2252
|
-
spanWidth = colSpan * dayWidth;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
// Convert to viewport coordinates
|
|
2256
|
-
const viewportY = yStart - scrollTop;
|
|
2257
|
-
|
|
2258
|
-
// Skip if completely out of view
|
|
2259
|
-
if (viewportY + eventHeight < 0 || viewportY > height) continue;
|
|
2260
|
-
|
|
2261
|
-
const eventColor = event.color || "#888888";
|
|
2262
|
-
const isSelected = this.internal.isEventSelected(event);
|
|
2263
|
-
const isHovered = this.hoveredEventId === event.id;
|
|
2264
|
-
|
|
2265
|
-
// Apple Calendar style dimensions
|
|
2266
|
-
const padding = 2;
|
|
2267
|
-
const eventWidth = spanWidth - padding * 2;
|
|
2268
|
-
const leftBorderWidth = 3;
|
|
2269
|
-
const contentPadding = 6;
|
|
2270
|
-
|
|
2271
|
-
// Determine border radius based on span position
|
|
2272
|
-
let radiusTopLeft = 4;
|
|
2273
|
-
let radiusTopRight = 4;
|
|
2274
|
-
let radiusBottomLeft = 4;
|
|
2275
|
-
let radiusBottomRight = 4;
|
|
2276
|
-
|
|
2277
|
-
if (totalWeeks > 1) {
|
|
2278
|
-
if (isStart && !isEnd) {
|
|
2279
|
-
radiusTopRight = 0;
|
|
2280
|
-
radiusBottomRight = 0;
|
|
2281
|
-
} else if (!isStart && isEnd) {
|
|
2282
|
-
radiusTopLeft = 0;
|
|
2283
|
-
radiusBottomLeft = 0;
|
|
2284
|
-
} else if (!isStart && !isEnd) {
|
|
2285
|
-
radiusTopLeft = 0;
|
|
2286
|
-
radiusTopRight = 0;
|
|
2287
|
-
radiusBottomLeft = 0;
|
|
2288
|
-
radiusBottomRight = 0;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
ctx.save();
|
|
2293
|
-
|
|
2294
|
-
// Apply transparency for read-only events
|
|
2295
|
-
if (event.readOnly) {
|
|
2296
|
-
ctx.globalAlpha = 0.5;
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
// Draw background - solid color if selected, subtle if not
|
|
2300
|
-
const hsl = rgbToHsl(hexToRgb(eventColor));
|
|
2301
|
-
const textPrimary =
|
|
2302
|
-
getComputedStyle(this).getPropertyValue("--text-primary").trim() ||
|
|
2303
|
-
"rgba(255, 255, 255, 0.9)";
|
|
2304
|
-
const textInverse =
|
|
2305
|
-
getComputedStyle(this).getPropertyValue("--text-inverse").trim() ||
|
|
2306
|
-
"rgb(0, 0, 0)";
|
|
2307
|
-
// Increase saturation to 75% for more vibrant colors, allow higher lightness for brighter backgrounds
|
|
2308
|
-
const backgroundColor = isSelected
|
|
2309
|
-
? eventColor
|
|
2310
|
-
: `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
|
|
2311
|
-
hsl[2] + 15,
|
|
2312
|
-
40,
|
|
2313
|
-
)}%, 0.45)`;
|
|
2314
|
-
const borderColor = isSelected
|
|
2315
|
-
? eventColor
|
|
2316
|
-
: `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
|
|
2317
|
-
hsl[2] + 10,
|
|
2318
|
-
70,
|
|
2319
|
-
)}%, 1)`;
|
|
2320
|
-
|
|
2321
|
-
// Check if event should have stripes (not accepted)
|
|
2322
|
-
const useStripes = this.shouldRenderEventWithStripes(event);
|
|
2323
|
-
|
|
2324
|
-
ctx.beginPath();
|
|
2325
|
-
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
2326
|
-
radiusTopLeft,
|
|
2327
|
-
radiusTopRight,
|
|
2328
|
-
radiusBottomRight,
|
|
2329
|
-
radiusBottomLeft,
|
|
2330
|
-
]);
|
|
2331
|
-
|
|
2332
|
-
// Fill with solid background first
|
|
2333
|
-
ctx.fillStyle = backgroundColor;
|
|
2334
|
-
ctx.fill();
|
|
2335
|
-
|
|
2336
|
-
// Add stripe pattern overlay if needed
|
|
2337
|
-
if (useStripes) {
|
|
2338
|
-
const patternCanvas = this.getStripePatternCanvas();
|
|
2339
|
-
if (patternCanvas) {
|
|
2340
|
-
ctx.save();
|
|
2341
|
-
|
|
2342
|
-
// Clip to the event rectangle
|
|
2343
|
-
ctx.beginPath();
|
|
2344
|
-
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
2345
|
-
radiusTopLeft,
|
|
2346
|
-
radiusTopRight,
|
|
2347
|
-
radiusBottomRight,
|
|
2348
|
-
radiusBottomLeft,
|
|
2349
|
-
]);
|
|
2350
|
-
ctx.clip();
|
|
2351
|
-
|
|
2352
|
-
// Pattern size is 16px (8 * scale of 2)
|
|
2353
|
-
const patternSize = 12;
|
|
2354
|
-
|
|
2355
|
-
// Calculate starting position to align with content coordinates
|
|
2356
|
-
// Start from a position that makes the pattern appear fixed in content space
|
|
2357
|
-
const startX = Math.floor((x + padding) / patternSize) * patternSize;
|
|
2358
|
-
const startY =
|
|
2359
|
-
Math.floor(yStart / patternSize) * patternSize - scrollTop;
|
|
2360
|
-
|
|
2361
|
-
// Tile the pattern across the clipped area
|
|
2362
|
-
for (
|
|
2363
|
-
let py = startY;
|
|
2364
|
-
py < viewportY + eventHeight;
|
|
2365
|
-
py += patternSize
|
|
2366
|
-
) {
|
|
2367
|
-
for (
|
|
2368
|
-
let px = startX;
|
|
2369
|
-
px < x + padding + eventWidth;
|
|
2370
|
-
px += patternSize
|
|
2371
|
-
) {
|
|
2372
|
-
ctx.drawImage(patternCanvas, px, py, patternSize, patternSize);
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
ctx.restore();
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
// Draw colored left border (Apple Calendar style)
|
|
2381
|
-
if (!isSelected) {
|
|
2382
|
-
ctx.fillStyle = borderColor;
|
|
2383
|
-
ctx.beginPath();
|
|
2384
|
-
ctx.roundRect(
|
|
2385
|
-
x + padding + 2,
|
|
2386
|
-
viewportY + 2,
|
|
2387
|
-
leftBorderWidth,
|
|
2388
|
-
eventHeight - 4,
|
|
2389
|
-
[radiusTopLeft, radiusTopLeft, radiusBottomLeft, radiusBottomLeft],
|
|
2390
|
-
);
|
|
2391
|
-
ctx.fill();
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
// Draw selection border - visible outline for selected events
|
|
2395
|
-
if (isSelected) {
|
|
2396
|
-
ctx.strokeStyle = textInverse;
|
|
2397
|
-
ctx.lineWidth = 1;
|
|
2398
|
-
ctx.beginPath();
|
|
2399
|
-
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
2400
|
-
radiusTopLeft,
|
|
2401
|
-
radiusTopRight,
|
|
2402
|
-
radiusBottomRight,
|
|
2403
|
-
radiusBottomLeft,
|
|
2404
|
-
]);
|
|
2405
|
-
ctx.stroke();
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
// Draw hover outline
|
|
2409
|
-
if (isHovered && !isSelected) {
|
|
2410
|
-
ctx.strokeStyle =
|
|
2411
|
-
getComputedStyle(this)
|
|
2412
|
-
.getPropertyValue("--grid-color-hover")
|
|
2413
|
-
.trim() || "rgba(255, 255, 255, 0.2)";
|
|
2414
|
-
ctx.lineWidth = 1;
|
|
2415
|
-
ctx.beginPath();
|
|
2416
|
-
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
2417
|
-
radiusTopLeft,
|
|
2418
|
-
radiusTopRight,
|
|
2419
|
-
radiusBottomRight,
|
|
2420
|
-
radiusBottomLeft,
|
|
2421
|
-
]);
|
|
2422
|
-
ctx.stroke();
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// Draw text if this is the start segment and there's enough space
|
|
2426
|
-
if (isStart && eventHeight >= 16) {
|
|
2427
|
-
// Use inverse text color for selected events (for contrast against solid color)
|
|
2428
|
-
// Use event color or primary text color for unselected events
|
|
2429
|
-
const textColor = isSelected
|
|
2430
|
-
? textInverse || "white"
|
|
2431
|
-
: textPrimary || eventColor;
|
|
2432
|
-
ctx.fillStyle = textColor;
|
|
2433
|
-
ctx.font = `11px ${fontFamily}`;
|
|
2434
|
-
ctx.textAlign = "left";
|
|
2435
|
-
ctx.textBaseline = "top";
|
|
2436
|
-
|
|
2437
|
-
const textX = x + padding + leftBorderWidth + contentPadding + 1;
|
|
2438
|
-
const textY = viewportY + 6;
|
|
2439
|
-
let maxTextWidth = eventWidth - leftBorderWidth - contentPadding - 4;
|
|
2440
|
-
|
|
2441
|
-
// Clip text to event bounds
|
|
2442
|
-
ctx.save();
|
|
2443
|
-
ctx.beginPath();
|
|
2444
|
-
ctx.rect(
|
|
2445
|
-
x + padding + leftBorderWidth,
|
|
2446
|
-
viewportY,
|
|
2447
|
-
eventWidth - leftBorderWidth,
|
|
2448
|
-
eventHeight,
|
|
2449
|
-
);
|
|
2450
|
-
ctx.clip();
|
|
2451
|
-
|
|
2452
|
-
// Draw recurring icon if event has rrule
|
|
2453
|
-
if (event.rrule) {
|
|
2454
|
-
ctx.font = `11px ${fontFamily}`;
|
|
2455
|
-
const recurIcon = "⟳";
|
|
2456
|
-
const iconWidth = ctx.measureText(recurIcon).width;
|
|
2457
|
-
ctx.fillText(recurIcon, textX, textY);
|
|
2458
|
-
maxTextWidth -= iconWidth + 4;
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
// Draw title with ellipsis if needed
|
|
2462
|
-
const titleStartX =
|
|
2463
|
-
textX + (event.rrule ? ctx.measureText("⟳").width + 4 : 0);
|
|
2464
|
-
let displayTitle = event.title;
|
|
2465
|
-
ctx.font = `11px ${fontFamily}`;
|
|
2466
|
-
const titleWidth = ctx.measureText(displayTitle).width;
|
|
2467
|
-
|
|
2468
|
-
if (titleWidth > maxTextWidth) {
|
|
2469
|
-
const ellipsis = "…";
|
|
2470
|
-
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
2471
|
-
|
|
2472
|
-
// Binary search for the right length
|
|
2473
|
-
let left = 0;
|
|
2474
|
-
let right = displayTitle.length;
|
|
2475
|
-
let bestFit = 0;
|
|
2476
|
-
|
|
2477
|
-
while (left <= right) {
|
|
2478
|
-
const mid = Math.floor((left + right) / 2);
|
|
2479
|
-
const testText = displayTitle.substring(0, mid);
|
|
2480
|
-
const testWidth = ctx.measureText(testText).width + ellipsisWidth;
|
|
2481
|
-
|
|
2482
|
-
if (testWidth <= maxTextWidth) {
|
|
2483
|
-
bestFit = mid;
|
|
2484
|
-
left = mid + 1;
|
|
2485
|
-
} else {
|
|
2486
|
-
right = mid - 1;
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
displayTitle = displayTitle.substring(0, bestFit) + ellipsis;
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
ctx.fillText(displayTitle, titleStartX, textY);
|
|
2494
|
-
|
|
2495
|
-
// Draw time if there's enough space (at least 32px for title + time)
|
|
2496
|
-
if (eventHeight >= 32) {
|
|
2497
|
-
const formatTime = (date: Date) => {
|
|
2498
|
-
const hours = date.getHours();
|
|
2499
|
-
const minutes = date.getMinutes();
|
|
2500
|
-
const ampm = hours >= 12 ? "PM" : "AM";
|
|
2501
|
-
const displayHours = hours % 12 || 12;
|
|
2502
|
-
return `${displayHours}:${minutes
|
|
2503
|
-
.toString()
|
|
2504
|
-
.padStart(2, "0")} ${ampm}`;
|
|
2505
|
-
};
|
|
2506
|
-
|
|
2507
|
-
const startTime = formatTime(event.start);
|
|
2508
|
-
const endTime = formatTime(event.end);
|
|
2509
|
-
const timeText = `${startTime} – ${endTime}`;
|
|
2510
|
-
|
|
2511
|
-
// Use inverse text color for selected events, primary text or event color for unselected
|
|
2512
|
-
const timeTextColor = isSelected
|
|
2513
|
-
? textInverse || "white"
|
|
2514
|
-
: textPrimary || eventColor;
|
|
2515
|
-
ctx.fillStyle = timeTextColor;
|
|
2516
|
-
ctx.font = `10px ${fontFamily}`;
|
|
2517
|
-
|
|
2518
|
-
// Draw time with ellipsis if needed
|
|
2519
|
-
let displayTime = timeText;
|
|
2520
|
-
let timeWidth = ctx.measureText(displayTime).width;
|
|
2521
|
-
|
|
2522
|
-
if (timeWidth > maxTextWidth) {
|
|
2523
|
-
const ellipsis = "…";
|
|
2524
|
-
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
2525
|
-
|
|
2526
|
-
let left = 0;
|
|
2527
|
-
let right = displayTime.length;
|
|
2528
|
-
let bestFit = 0;
|
|
2529
|
-
|
|
2530
|
-
while (left <= right) {
|
|
2531
|
-
const mid = Math.floor((left + right) / 2);
|
|
2532
|
-
const testText = displayTime.substring(0, mid);
|
|
2533
|
-
const testWidth = ctx.measureText(testText).width + ellipsisWidth;
|
|
2534
|
-
|
|
2535
|
-
if (testWidth <= maxTextWidth) {
|
|
2536
|
-
bestFit = mid;
|
|
2537
|
-
left = mid + 1;
|
|
2538
|
-
} else {
|
|
2539
|
-
right = mid - 1;
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
displayTime = displayTime.substring(0, bestFit) + ellipsis;
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
ctx.fillText(displayTime, textX, textY + 14);
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
ctx.restore();
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
ctx.restore();
|
|
2553
|
-
|
|
2554
|
-
// Store rect for hit testing (in content coordinates)
|
|
2555
|
-
this.eventRects.push({
|
|
2556
|
-
event,
|
|
2557
|
-
x: x + padding,
|
|
2558
|
-
y: yStart,
|
|
2559
|
-
width: eventWidth,
|
|
2560
|
-
height: eventHeight,
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
// Render month labels
|
|
2565
|
-
const monthNames = [
|
|
2566
|
-
"January",
|
|
2567
|
-
"February",
|
|
2568
|
-
"March",
|
|
2569
|
-
"April",
|
|
2570
|
-
"May",
|
|
2571
|
-
"June",
|
|
2572
|
-
"July",
|
|
2573
|
-
"August",
|
|
2574
|
-
"September",
|
|
2575
|
-
"October",
|
|
2576
|
-
"November",
|
|
2577
|
-
"December",
|
|
2578
|
-
];
|
|
2579
|
-
|
|
2580
|
-
const monthBoundaries: {
|
|
2581
|
-
monthKey: string;
|
|
2582
|
-
monthName: string;
|
|
2583
|
-
year: number;
|
|
2584
|
-
yOffset: number;
|
|
2585
|
-
}[] = [];
|
|
2586
|
-
const seenMonths = new Set<string>();
|
|
2587
|
-
|
|
2588
|
-
for (const week of visibleWeeks) {
|
|
2589
|
-
const firstDay = week.days[0];
|
|
2590
|
-
if (!firstDay) continue;
|
|
2591
|
-
|
|
2592
|
-
const monthIndex = firstDay.getMonth();
|
|
2593
|
-
const year = firstDay.getFullYear();
|
|
2594
|
-
const monthKey = `${monthIndex}-${year}`;
|
|
2595
|
-
|
|
2596
|
-
if (!seenMonths.has(monthKey)) {
|
|
2597
|
-
seenMonths.add(monthKey);
|
|
2598
|
-
const monthName = monthNames[monthIndex];
|
|
2599
|
-
if (monthName) {
|
|
2600
|
-
monthBoundaries.push({
|
|
2601
|
-
monthKey,
|
|
2602
|
-
monthName,
|
|
2603
|
-
year,
|
|
2604
|
-
yOffset: week.yOffset,
|
|
2605
|
-
});
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
for (let i = 0; i < monthBoundaries.length; i++) {
|
|
2611
|
-
const month = monthBoundaries[i]!;
|
|
2612
|
-
const nextMonth = monthBoundaries[i + 1];
|
|
2613
|
-
const labelY = month.yOffset;
|
|
2614
|
-
const nextMonthY = nextMonth ? nextMonth.yOffset : this.totalHeight;
|
|
2615
|
-
|
|
2616
|
-
if (nextMonthY < scrollTop) continue;
|
|
2617
|
-
if (labelY > viewportBottom) break;
|
|
2618
|
-
|
|
2619
|
-
const padding = [12, 0, 0, 12];
|
|
2620
|
-
|
|
2621
|
-
const stickyTop = Math.max(0, scrollTop - labelY);
|
|
2622
|
-
const maxStickyTop = nextMonthY - labelY - 24;
|
|
2623
|
-
const clampedStickyTop = Math.min(stickyTop, maxStickyTop);
|
|
2624
|
-
const labelTopMargin = 32;
|
|
2625
|
-
const finalTop = labelY + clampedStickyTop - scrollTop + labelTopMargin;
|
|
2626
|
-
|
|
2627
|
-
ctx.save();
|
|
2628
|
-
ctx.font = `bold 18px ${fontFamily}`;
|
|
2629
|
-
ctx.textAlign = "left";
|
|
2630
|
-
ctx.textBaseline = "top";
|
|
2631
|
-
|
|
2632
|
-
const labelText = `${month.monthName} ${month.year}`;
|
|
2633
|
-
const textWidth = ctx.measureText(labelText).width;
|
|
2634
|
-
const leftMargin = 8;
|
|
2635
|
-
const textX = 64 + padding[3] + leftMargin;
|
|
2636
|
-
const textY = finalTop + padding[0];
|
|
2637
|
-
|
|
2638
|
-
// Draw background
|
|
2639
|
-
const bgPaddingLeft = 8;
|
|
2640
|
-
const bgPaddingRight = 8;
|
|
2641
|
-
const bgElevated =
|
|
2642
|
-
getComputedStyle(this).getPropertyValue("--bg-elevated").trim() ||
|
|
2643
|
-
"rgba(0, 0, 0, 0.7)";
|
|
2644
|
-
ctx.fillStyle = bgElevated;
|
|
2645
|
-
ctx.beginPath();
|
|
2646
|
-
ctx.roundRect(
|
|
2647
|
-
textX - bgPaddingLeft,
|
|
2648
|
-
textY - 4,
|
|
2649
|
-
textWidth + bgPaddingLeft + bgPaddingRight,
|
|
2650
|
-
26,
|
|
2651
|
-
6,
|
|
2652
|
-
);
|
|
2653
|
-
ctx.fill();
|
|
2654
|
-
|
|
2655
|
-
// Draw text
|
|
2656
|
-
const textPrimary =
|
|
2657
|
-
getComputedStyle(this).getPropertyValue("--text-primary").trim() ||
|
|
2658
|
-
"rgba(255, 255, 255, 0.95)";
|
|
2659
|
-
ctx.fillStyle = textPrimary;
|
|
2660
|
-
ctx.fillText(labelText, textX, textY);
|
|
2661
|
-
ctx.restore();
|
|
2662
|
-
}
|
|
1900
|
+
toggleLayer(name: string): void {
|
|
1901
|
+
const layer = this.layers.find((l) => l.name === name);
|
|
1902
|
+
if (!layer) throw new Error(`Layer "${name}" not found`);
|
|
1903
|
+
layer.enabled = !layer.enabled;
|
|
1904
|
+
this.renderCanvas();
|
|
2663
1905
|
}
|
|
2664
1906
|
|
|
1907
|
+
|
|
2665
1908
|
onWheel = (e: WheelEvent): void => {
|
|
2666
1909
|
if (this.hasAttribute("scroll-lock")) return;
|
|
1910
|
+
this.lastWheelTime = Date.now();
|
|
2667
1911
|
|
|
2668
1912
|
if (this.scrollAnimationFrame) {
|
|
2669
1913
|
this.cancelScrollAnimation();
|
|
@@ -3051,9 +2295,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
3051
2295
|
snappedEnd.setTime(snappedStart.getTime() + 15 * 60 * 1000);
|
|
3052
2296
|
}
|
|
3053
2297
|
|
|
2298
|
+
const isZoomedOut = this.dayHeight < TIME_SCALE_DAY_HEIGHT;
|
|
3054
2299
|
this.dispatchEvent(
|
|
3055
2300
|
new CustomEvent("create-event", {
|
|
3056
|
-
detail: { start: snappedStart, end: snappedEnd },
|
|
2301
|
+
detail: { start: snappedStart, end: snappedEnd, isAllDay: isZoomedOut },
|
|
3057
2302
|
bubbles: true,
|
|
3058
2303
|
}),
|
|
3059
2304
|
);
|
|
@@ -3119,6 +2364,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
3119
2364
|
this.resizingOriginalStart = null;
|
|
3120
2365
|
this.resizingOriginalEnd = null;
|
|
3121
2366
|
this.isResizingEvent = false;
|
|
2367
|
+
if (this.scrollContainer) {
|
|
2368
|
+
this.scrollContainer.style.cursor = "";
|
|
2369
|
+
}
|
|
3122
2370
|
this.renderCanvas();
|
|
3123
2371
|
this.requestUpdate();
|
|
3124
2372
|
}
|
|
@@ -3138,6 +2386,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3138
2386
|
// Close event detail overlay
|
|
3139
2387
|
this.selectedEventForDetail = null;
|
|
3140
2388
|
this.selectedEventRect = null;
|
|
2389
|
+
this.clearDescriptionSummary();
|
|
3141
2390
|
|
|
3142
2391
|
if (hadSelection) {
|
|
3143
2392
|
this.dispatchEvent(
|
|
@@ -3188,6 +2437,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3188
2437
|
this.internal.clearSelection();
|
|
3189
2438
|
this.selectedEventForDetail = null;
|
|
3190
2439
|
this.selectedEventRect = null;
|
|
2440
|
+
this.clearDescriptionSummary();
|
|
3191
2441
|
this.dispatchEvent(
|
|
3192
2442
|
new CustomEvent("selection-change", {
|
|
3193
2443
|
detail: { selectedEvents: [] },
|
|
@@ -3220,6 +2470,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3220
2470
|
this.selectedEventForDetail = null;
|
|
3221
2471
|
this.selectedEventRect = null;
|
|
3222
2472
|
this.isDescriptionExpanded = false;
|
|
2473
|
+
this.clearDescriptionSummary();
|
|
3223
2474
|
this.renderCanvas();
|
|
3224
2475
|
this.requestUpdate();
|
|
3225
2476
|
this.dispatchEvent(
|
|
@@ -3241,16 +2492,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
3241
2492
|
// Update cursor position for status bar
|
|
3242
2493
|
this.cursorPosition = { x, y };
|
|
3243
2494
|
|
|
3244
|
-
// Check for resize handles first
|
|
3245
|
-
const resizeHandle = this.getResizeHandle(x, y);
|
|
2495
|
+
// Check for resize handles first — suppressed when alt key is active
|
|
2496
|
+
const resizeHandle = !e.altKey ? this.getResizeHandle(x, y) : null;
|
|
3246
2497
|
if (resizeHandle && !this.isResizingEvent && !this.isDraggingEvent) {
|
|
3247
|
-
this.scrollContainer.style.cursor =
|
|
2498
|
+
this.scrollContainer.style.cursor =
|
|
2499
|
+
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
3248
2500
|
} else if (!this.isResizingEvent && !this.isDraggingEvent) {
|
|
3249
2501
|
this.scrollContainer.style.cursor = "";
|
|
3250
2502
|
}
|
|
3251
2503
|
|
|
3252
|
-
// Check for event hover
|
|
3253
|
-
const hoveredEvent = this.getEventAtPosition(x, y);
|
|
2504
|
+
// Check for event hover — suppressed when alt key is active
|
|
2505
|
+
const hoveredEvent = !e.altKey ? this.getEventAtPosition(x, y) : null;
|
|
3254
2506
|
const newHoveredId = hoveredEvent ? hoveredEvent.id : null;
|
|
3255
2507
|
|
|
3256
2508
|
if (newHoveredId !== this.hoveredEventId) {
|
|
@@ -3383,11 +2635,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
3383
2635
|
} else {
|
|
3384
2636
|
// Move event
|
|
3385
2637
|
this.dispatchEvent(
|
|
3386
|
-
new CustomEvent("
|
|
2638
|
+
new CustomEvent("update-event", {
|
|
3387
2639
|
detail: {
|
|
3388
2640
|
event: this.movingEvent,
|
|
3389
|
-
start: newStart,
|
|
3390
|
-
end: newEnd,
|
|
2641
|
+
updates: { start: newStart, end: newEnd },
|
|
3391
2642
|
},
|
|
3392
2643
|
bubbles: true,
|
|
3393
2644
|
}),
|
|
@@ -3408,6 +2659,31 @@ export class CalendarViewElement extends LitElement {
|
|
|
3408
2659
|
}
|
|
3409
2660
|
};
|
|
3410
2661
|
|
|
2662
|
+
onKeyDown = (e: KeyboardEvent): void => {
|
|
2663
|
+
const focused = e.composedPath()[0] as HTMLElement;
|
|
2664
|
+
if (focused?.tagName === "INPUT" || focused?.tagName === "TEXTAREA" || focused?.isContentEditable) return;
|
|
2665
|
+
if (e.altKey && !this.altKeyActive) {
|
|
2666
|
+
this.altKeyActive = true;
|
|
2667
|
+
this.dispatchEvent(new CustomEvent("meta-key-change", { bubbles: true }));
|
|
2668
|
+
if (this.scrollContainer) {
|
|
2669
|
+
this.scrollContainer.style.cursor = "";
|
|
2670
|
+
}
|
|
2671
|
+
if (this.hoveredEventId !== null) {
|
|
2672
|
+
this.hoveredEventId = null;
|
|
2673
|
+
this.renderCanvas();
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
|
|
2678
|
+
onKeyUp = (e: KeyboardEvent): void => {
|
|
2679
|
+
const focused = e.composedPath()[0] as HTMLElement;
|
|
2680
|
+
if (focused?.tagName === "INPUT" || focused?.tagName === "TEXTAREA" || focused?.isContentEditable) return;
|
|
2681
|
+
if (!e.altKey && this.altKeyActive) {
|
|
2682
|
+
this.altKeyActive = false;
|
|
2683
|
+
this.dispatchEvent(new CustomEvent("meta-key-change", { bubbles: true }));
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
|
|
3411
2687
|
onPaste = async (e: ClipboardEvent): Promise<void> => {
|
|
3412
2688
|
const files = e.clipboardData?.files;
|
|
3413
2689
|
if (files && files.length > 0) {
|
|
@@ -3522,6 +2798,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3522
2798
|
y: number,
|
|
3523
2799
|
): { event: CalendarEvent; edge: "start" | "end" } | null {
|
|
3524
2800
|
const RESIZE_HANDLE_SIZE = 8; // pixels from edge to detect resize
|
|
2801
|
+
const isZoomedIn = this.dayHeight >= TIME_SCALE_DAY_HEIGHT;
|
|
3525
2802
|
|
|
3526
2803
|
// Check in reverse order (top to bottom rendering)
|
|
3527
2804
|
for (let i = this.eventRects.length - 1; i >= 0; i--) {
|
|
@@ -3531,18 +2808,33 @@ export class CalendarViewElement extends LitElement {
|
|
|
3531
2808
|
// Skip read-only events
|
|
3532
2809
|
if (rect.event.readOnly) continue;
|
|
3533
2810
|
|
|
3534
|
-
|
|
3535
|
-
|
|
2811
|
+
if (isZoomedIn) {
|
|
2812
|
+
// In time-scale view, resize vertically from the top/bottom edges.
|
|
2813
|
+
if (x < rect.x || x > rect.x + rect.width) continue;
|
|
2814
|
+
|
|
2815
|
+
if (y >= rect.y && y <= rect.y + RESIZE_HANDLE_SIZE) {
|
|
2816
|
+
return { event: rect.event, edge: "start" };
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
if (
|
|
2820
|
+
y >= rect.y + rect.height - RESIZE_HANDLE_SIZE &&
|
|
2821
|
+
y <= rect.y + rect.height
|
|
2822
|
+
) {
|
|
2823
|
+
return { event: rect.event, edge: "end" };
|
|
2824
|
+
}
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
3536
2827
|
|
|
3537
|
-
//
|
|
3538
|
-
if (y
|
|
2828
|
+
// In zoomed-out view, resize horizontally from the left/right edges.
|
|
2829
|
+
if (y < rect.y || y > rect.y + rect.height) continue;
|
|
2830
|
+
|
|
2831
|
+
if (x >= rect.x && x <= rect.x + RESIZE_HANDLE_SIZE) {
|
|
3539
2832
|
return { event: rect.event, edge: "start" };
|
|
3540
2833
|
}
|
|
3541
2834
|
|
|
3542
|
-
// Check bottom edge (resize end)
|
|
3543
2835
|
if (
|
|
3544
|
-
|
|
3545
|
-
|
|
2836
|
+
x >= rect.x + rect.width - RESIZE_HANDLE_SIZE &&
|
|
2837
|
+
x <= rect.x + rect.width
|
|
3546
2838
|
) {
|
|
3547
2839
|
return { event: rect.event, edge: "end" };
|
|
3548
2840
|
}
|
|
@@ -3582,6 +2874,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
3582
2874
|
this.resizingOriginalStart = new Date(resizeHandle.event.start);
|
|
3583
2875
|
this.resizingOriginalEnd = new Date(resizeHandle.event.end);
|
|
3584
2876
|
this.isResizingEvent = false; // Will be set to true on first mouse move
|
|
2877
|
+
this.scrollContainer.style.cursor =
|
|
2878
|
+
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
3585
2879
|
return;
|
|
3586
2880
|
}
|
|
3587
2881
|
|
|
@@ -3604,6 +2898,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3604
2898
|
// Close event detail overlay
|
|
3605
2899
|
this.selectedEventForDetail = null;
|
|
3606
2900
|
this.selectedEventRect = null;
|
|
2901
|
+
this.clearDescriptionSummary();
|
|
3607
2902
|
|
|
3608
2903
|
this.movingEvent = clickedEvent;
|
|
3609
2904
|
this.movingEventOrigin = { x, y };
|
|
@@ -3619,6 +2914,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3619
2914
|
// Close event detail overlay when clicking outside of events
|
|
3620
2915
|
this.selectedEventForDetail = null;
|
|
3621
2916
|
this.selectedEventRect = null;
|
|
2917
|
+
this.clearDescriptionSummary();
|
|
3622
2918
|
|
|
3623
2919
|
// Clear selection when clicking on empty space
|
|
3624
2920
|
const hadSelection = this.internal.getSelectedEvents().length > 0;
|
|
@@ -3653,13 +2949,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
3653
2949
|
*
|
|
3654
2950
|
* Selection box is 2D with different behavior based on zoom level:
|
|
3655
2951
|
*
|
|
3656
|
-
* ZOOMED OUT (dayHeight <
|
|
2952
|
+
* ZOOMED OUT (dayHeight < TIME_SCALE_DAY_HEIGHT):
|
|
3657
2953
|
* - X axis: days of the week (horizontal)
|
|
3658
2954
|
* - Y axis: multiple weeks stacked (vertical)
|
|
3659
2955
|
* - Creates time ranges per WEEK for selected day range
|
|
3660
2956
|
* - Example: Wed-Thu across 3 weeks = 3 ranges (one per week)
|
|
3661
2957
|
*
|
|
3662
|
-
* ZOOMED IN (dayHeight >=
|
|
2958
|
+
* ZOOMED IN (dayHeight >= TIME_SCALE_DAY_HEIGHT, time scale visible):
|
|
3663
2959
|
* - X axis: days of the week (horizontal)
|
|
3664
2960
|
* - Y axis: time within days (vertical, with weeks)
|
|
3665
2961
|
* - Creates time ranges per DAY for selected time range
|
|
@@ -3705,7 +3001,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3705
3001
|
if (intersectingWeeks.length === 0) return;
|
|
3706
3002
|
|
|
3707
3003
|
const timeRanges: Array<{ start: Date; end: Date }> = [];
|
|
3708
|
-
const isZoomedIn = this.dayHeight >=
|
|
3004
|
+
const isZoomedIn = this.dayHeight >= TIME_SCALE_DAY_HEIGHT;
|
|
3709
3005
|
|
|
3710
3006
|
if (isZoomedIn) {
|
|
3711
3007
|
// ZOOMED IN: Create time ranges per DAY (same time across multiple weeks)
|
|
@@ -3794,6 +3090,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
3794
3090
|
for (const event of this.events) {
|
|
3795
3091
|
// Skip all-day events
|
|
3796
3092
|
if (event.isAllDay) continue;
|
|
3093
|
+
if (event.visualStyle === "heatmap") continue;
|
|
3094
|
+
|
|
3095
|
+
// Only select events from the active calendar
|
|
3096
|
+
if (this.activeCalendarId && event.calendarId !== this.activeCalendarId && event.sourceId !== this.activeCalendarId) continue;
|
|
3797
3097
|
|
|
3798
3098
|
const eventStartTime = event.start.getTime();
|
|
3799
3099
|
const eventEndTime = event.end.getTime();
|
|
@@ -3822,6 +3122,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3822
3122
|
// Close event detail overlay when replacing selection
|
|
3823
3123
|
this.selectedEventForDetail = null;
|
|
3824
3124
|
this.selectedEventRect = null;
|
|
3125
|
+
this.clearDescriptionSummary();
|
|
3825
3126
|
}
|
|
3826
3127
|
for (const event of selectedEvents) {
|
|
3827
3128
|
this.internal.selectEvent(event, "add");
|
|
@@ -3885,9 +3186,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
3885
3186
|
const scrollTop = this.scrollTop;
|
|
3886
3187
|
const scrollBottom = scrollTop + this.viewportHeight;
|
|
3887
3188
|
|
|
3189
|
+
const nearTop = scrollTop < bufferHeight;
|
|
3190
|
+
const nearBottom = scrollBottom > this.totalHeight - bufferHeight;
|
|
3191
|
+
if (!nearTop && !nearBottom) return;
|
|
3192
|
+
|
|
3888
3193
|
// Check if near top (extending to past)
|
|
3889
|
-
if (
|
|
3194
|
+
if (nearTop) {
|
|
3890
3195
|
this.isExtendingRange = true;
|
|
3196
|
+
// Capture before extension: only compensate for wheel/touch scroll.
|
|
3197
|
+
// For scrollbar drags, skip compensation so the thumb stays where the user
|
|
3198
|
+
// put it and they see the newly loaded historical content — no treadmill.
|
|
3199
|
+
const isWheelScroll = Date.now() - this.lastWheelTime < 200;
|
|
3891
3200
|
const newWeeks = this.internal.extendRange("past");
|
|
3892
3201
|
|
|
3893
3202
|
// Prepend new weeks
|
|
@@ -3906,11 +3215,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
3906
3215
|
// Must update DOM first so scroll container has correct height
|
|
3907
3216
|
this.requestUpdate();
|
|
3908
3217
|
|
|
3909
|
-
// Adjust scroll position to maintain visual position (after DOM update)
|
|
3910
3218
|
requestAnimationFrame(() => {
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
this.
|
|
3219
|
+
if (isWheelScroll) {
|
|
3220
|
+
// Maintain visual position so content doesn't jump during wheel scroll
|
|
3221
|
+
this._scrollTop += addedHeight;
|
|
3222
|
+
if (this.scrollContainer) {
|
|
3223
|
+
this.scrollContainer.scrollTop = this._scrollTop;
|
|
3224
|
+
}
|
|
3914
3225
|
}
|
|
3915
3226
|
|
|
3916
3227
|
this.renderCanvas();
|
|
@@ -3921,7 +3232,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3921
3232
|
}
|
|
3922
3233
|
|
|
3923
3234
|
// Check if near bottom (extending to future)
|
|
3924
|
-
if (
|
|
3235
|
+
if (nearBottom) {
|
|
3925
3236
|
this.isExtendingRange = true;
|
|
3926
3237
|
const newWeeks = this.internal.extendRange("future");
|
|
3927
3238
|
|
|
@@ -4021,6 +3332,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4021
3332
|
|
|
4022
3333
|
// Show event detail overlay for single selection
|
|
4023
3334
|
if (!isCmdOrCtrl) {
|
|
3335
|
+
this.clearDescriptionSummary();
|
|
4024
3336
|
this.selectedEventForDetail = event;
|
|
4025
3337
|
this.isDescriptionExpanded = false;
|
|
4026
3338
|
// Find the event's rect and store it for positioning
|
|
@@ -4040,6 +3352,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4040
3352
|
this.selectedEventForDetail = null;
|
|
4041
3353
|
this.selectedEventRect = null;
|
|
4042
3354
|
this.isDescriptionExpanded = false;
|
|
3355
|
+
this.clearDescriptionSummary();
|
|
4043
3356
|
this.requestUpdate();
|
|
4044
3357
|
}
|
|
4045
3358
|
|
|
@@ -4101,6 +3414,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4101
3414
|
// Close event detail overlay when clearing selection
|
|
4102
3415
|
this.selectedEventForDetail = null;
|
|
4103
3416
|
this.selectedEventRect = null;
|
|
3417
|
+
this.clearDescriptionSummary();
|
|
4104
3418
|
|
|
4105
3419
|
if (hadSelection) {
|
|
4106
3420
|
this.dispatchEvent(
|
|
@@ -4194,6 +3508,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4194
3508
|
fill: accent.replace("rgb", "rgba").replace(")", ", 0.3)"),
|
|
4195
3509
|
stroke: accent.replace("rgb", "rgba").replace(")", ", 0.8)"),
|
|
4196
3510
|
text: "white",
|
|
3511
|
+
dashed: false,
|
|
4197
3512
|
},
|
|
4198
3513
|
useStartEdge,
|
|
4199
3514
|
);
|
|
@@ -4206,6 +3521,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4206
3521
|
fill: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
4207
3522
|
stroke: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
4208
3523
|
text: "rgba(255, 255, 255, 0.5)",
|
|
3524
|
+
dashed: false,
|
|
4209
3525
|
},
|
|
4210
3526
|
useStartEdge,
|
|
4211
3527
|
);
|
|
@@ -4250,8 +3566,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
4250
3566
|
later = startDate < endDate ? endDate : startDate;
|
|
4251
3567
|
}
|
|
4252
3568
|
|
|
4253
|
-
|
|
4254
|
-
|
|
3569
|
+
if (this.dayHeight < TIME_SCALE_DAY_HEIGHT) {
|
|
3570
|
+
earlier.setHours(0, 0, 0, 0);
|
|
3571
|
+
later.setHours(23, 59, 59, 999);
|
|
3572
|
+
} else {
|
|
3573
|
+
earlier.setMinutes(Math.round(earlier.getMinutes() / 15) * 15, 0, 0);
|
|
3574
|
+
later.setMinutes(Math.round(later.getMinutes() / 15) * 15, 0, 0);
|
|
3575
|
+
}
|
|
4255
3576
|
|
|
4256
3577
|
// Use active calendar color if available, otherwise fall back to accent-primary
|
|
4257
3578
|
let fill: string;
|
|
@@ -4279,7 +3600,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4279
3600
|
renderVirtualEvent(
|
|
4280
3601
|
start: Date,
|
|
4281
3602
|
end: Date,
|
|
4282
|
-
color: { fill: string; stroke: string; text: string },
|
|
3603
|
+
color: { fill: string; stroke: string; text: string; dashed?: boolean },
|
|
4283
3604
|
useStartEdge = true,
|
|
4284
3605
|
): void {
|
|
4285
3606
|
if (!this.overlayCanvas || !this.overlayCtx || !this.scrollContainer)
|
|
@@ -4329,7 +3650,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4329
3650
|
ctx.fill();
|
|
4330
3651
|
ctx.strokeStyle = color.stroke;
|
|
4331
3652
|
ctx.lineWidth = 1;
|
|
4332
|
-
ctx.setLineDash([6, 3]);
|
|
3653
|
+
if (color.dashed !== false) ctx.setLineDash([6, 3]);
|
|
4333
3654
|
ctx.stroke();
|
|
4334
3655
|
ctx.setLineDash([]);
|
|
4335
3656
|
};
|
|
@@ -4813,7 +4134,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4813
4134
|
let hour = 0;
|
|
4814
4135
|
let minute = 0;
|
|
4815
4136
|
|
|
4816
|
-
if (this.dayHeight >=
|
|
4137
|
+
if (this.dayHeight >= TIME_SCALE_DAY_HEIGHT) {
|
|
4817
4138
|
const offsetInRow = y - (week.yOffset + rowInWeek * this.dayHeight);
|
|
4818
4139
|
const minutes = Math.floor((offsetInRow / this.dayHeight) * 24 * 60);
|
|
4819
4140
|
hour = Math.floor(minutes / 60);
|
|
@@ -4897,6 +4218,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4897
4218
|
formattedCursorDate: cursorTime
|
|
4898
4219
|
? this.formatCompactDate(cursorTime.date)
|
|
4899
4220
|
: "",
|
|
4221
|
+
altKeyActive: this.altKeyActive,
|
|
4900
4222
|
};
|
|
4901
4223
|
}
|
|
4902
4224
|
|
|
@@ -4940,6 +4262,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4940
4262
|
|
|
4941
4263
|
this.selectedEventForDetail = null;
|
|
4942
4264
|
this.selectedEventRect = null;
|
|
4265
|
+
this.clearDescriptionSummary();
|
|
4943
4266
|
this.requestUpdate();
|
|
4944
4267
|
}
|
|
4945
4268
|
|
|
@@ -5079,6 +4402,31 @@ export class CalendarViewElement extends LitElement {
|
|
|
5079
4402
|
}
|
|
5080
4403
|
|
|
5081
4404
|
const event = this.selectedEventForDetail;
|
|
4405
|
+
const eventDescription = sanitizeEventDescription(event.description ?? "");
|
|
4406
|
+
const eventDescriptionText = eventDescription.replaceAll("\\n", "\n");
|
|
4407
|
+
const useDescriptionSummary = eventDescription.trim().length > 200;
|
|
4408
|
+
const currentSummaryKey = this.getDescriptionSummaryTargetKey(event);
|
|
4409
|
+
const hasRequestedSummaryForCurrentEvent =
|
|
4410
|
+
this.requestedDescriptionSummaryKey === currentSummaryKey;
|
|
4411
|
+
const isSummaryForCurrentEvent =
|
|
4412
|
+
this.descriptionSummaryTargetKey === currentSummaryKey;
|
|
4413
|
+
const hasSummaryStateForCurrentEvent =
|
|
4414
|
+
hasRequestedSummaryForCurrentEvent || isSummaryForCurrentEvent;
|
|
4415
|
+
const streamedSummaryText = hasSummaryStateForCurrentEvent
|
|
4416
|
+
? this.descriptionSummaryText
|
|
4417
|
+
: "";
|
|
4418
|
+
const isSummaryLoading =
|
|
4419
|
+
useDescriptionSummary && hasSummaryStateForCurrentEvent && this.descriptionSummaryLoading;
|
|
4420
|
+
const shouldRenderSummary =
|
|
4421
|
+
useDescriptionSummary && hasSummaryStateForCurrentEvent;
|
|
4422
|
+
const descriptionToRender = shouldRenderSummary
|
|
4423
|
+
? streamedSummaryText || (isSummaryLoading ? "Generating summary..." : eventDescriptionText)
|
|
4424
|
+
: eventDescriptionText;
|
|
4425
|
+
const summarizeButtonLabel = isSummaryLoading
|
|
4426
|
+
? "Summarizing..."
|
|
4427
|
+
: shouldRenderSummary
|
|
4428
|
+
? "Summarize again"
|
|
4429
|
+
: "Summarize";
|
|
5082
4430
|
|
|
5083
4431
|
const formatDate = (date: Date) => {
|
|
5084
4432
|
return new Intl.DateTimeFormat(this.locale, {
|
|
@@ -5217,6 +4565,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
5217
4565
|
this.selectedEventForDetail = null;
|
|
5218
4566
|
this.selectedEventRect = null;
|
|
5219
4567
|
this.isDescriptionExpanded = false;
|
|
4568
|
+
this.clearDescriptionSummary();
|
|
5220
4569
|
this.requestUpdate();
|
|
5221
4570
|
}}
|
|
5222
4571
|
>×</button>
|
|
@@ -5224,7 +4573,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
5224
4573
|
|
|
5225
4574
|
<div class="event-detail-body">
|
|
5226
4575
|
${(() => {
|
|
5227
|
-
const teamsMatch =
|
|
4576
|
+
const teamsMatch = eventDescription?.match(/https:\/\/teams\.microsoft\.com\/[^\s<>"]+/);
|
|
5228
4577
|
const teamsUrl = teamsMatch ? teamsMatch[0] : null;
|
|
5229
4578
|
if (!event.location && !teamsUrl) return null;
|
|
5230
4579
|
return html`
|
|
@@ -5327,47 +4676,84 @@ export class CalendarViewElement extends LitElement {
|
|
|
5327
4676
|
}
|
|
5328
4677
|
|
|
5329
4678
|
${
|
|
5330
|
-
|
|
5331
|
-
(event.organizer != null &&
|
|
5332
|
-
!this.currentUserEmails.has(event.organizer.email))
|
|
5333
|
-
? event.description
|
|
4679
|
+
useDescriptionSummary
|
|
5334
4680
|
? html`
|
|
5335
4681
|
<div class="event-detail-section">
|
|
5336
4682
|
<div class="event-detail-label">Description</div>
|
|
5337
4683
|
<div class="event-detail-description ${
|
|
5338
|
-
this.isDescriptionExpanded
|
|
4684
|
+
shouldRenderSummary || this.isDescriptionExpanded
|
|
4685
|
+
? "expanded"
|
|
4686
|
+
: ""
|
|
5339
4687
|
}">
|
|
5340
|
-
<pre class="event-detail-value">${
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
4688
|
+
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
4689
|
+
</div>
|
|
4690
|
+
${
|
|
4691
|
+
shouldRenderSummary && this.descriptionSummaryError
|
|
4692
|
+
? html`<div class="event-detail-value" style="opacity: 0.7;">Summary unavailable: ${this.descriptionSummaryError}. Showing original description.</div>`
|
|
4693
|
+
: null
|
|
4694
|
+
}
|
|
4695
|
+
<div class="description-actions">
|
|
4696
|
+
${
|
|
4697
|
+
!shouldRenderSummary
|
|
4698
|
+
? html`<button
|
|
4699
|
+
class="description-see-more"
|
|
4700
|
+
@click=${() => {
|
|
4701
|
+
this.isDescriptionExpanded = !this.isDescriptionExpanded;
|
|
4702
|
+
this.requestUpdate();
|
|
4703
|
+
}}
|
|
4704
|
+
>
|
|
4705
|
+
${this.isDescriptionExpanded ? "Show less" : "Show more"}
|
|
4706
|
+
</button>`
|
|
4707
|
+
: null
|
|
4708
|
+
}
|
|
4709
|
+
<button
|
|
4710
|
+
class="description-see-more"
|
|
4711
|
+
?disabled=${isSummaryLoading}
|
|
4712
|
+
@click=${() => this.requestDescriptionSummary(event)}
|
|
4713
|
+
>
|
|
4714
|
+
${summarizeButtonLabel}
|
|
4715
|
+
</button>
|
|
4716
|
+
</div>
|
|
4717
|
+
</div>
|
|
4718
|
+
`
|
|
4719
|
+
: event.readOnly ||
|
|
4720
|
+
(event.organizer != null &&
|
|
4721
|
+
!this.currentUserEmails.has(event.organizer.email))
|
|
4722
|
+
? eventDescription
|
|
4723
|
+
? html`
|
|
4724
|
+
<div class="event-detail-section">
|
|
4725
|
+
<div class="event-detail-label">Description</div>
|
|
4726
|
+
<div class="event-detail-description ${
|
|
4727
|
+
this.isDescriptionExpanded ? "expanded" : ""
|
|
4728
|
+
}">
|
|
4729
|
+
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
5344
4730
|
</div>
|
|
5345
4731
|
${
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
4732
|
+
eventDescription.length > 300 ||
|
|
4733
|
+
eventDescription.split("\n").length > 8
|
|
4734
|
+
? html`
|
|
5349
4735
|
<button
|
|
5350
4736
|
class="description-see-more"
|
|
5351
4737
|
@click=${() => {
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
4738
|
+
this.isDescriptionExpanded =
|
|
4739
|
+
!this.isDescriptionExpanded;
|
|
4740
|
+
this.requestUpdate();
|
|
4741
|
+
}}
|
|
5356
4742
|
>
|
|
5357
4743
|
${this.isDescriptionExpanded ? "See less" : "See more"}
|
|
5358
4744
|
</button>
|
|
5359
4745
|
`
|
|
5360
|
-
|
|
5361
|
-
|
|
4746
|
+
: null
|
|
4747
|
+
}
|
|
5362
4748
|
</div>
|
|
5363
4749
|
`
|
|
5364
|
-
|
|
5365
|
-
|
|
4750
|
+
: null
|
|
4751
|
+
: html`
|
|
5366
4752
|
<div class="event-detail-section">
|
|
5367
4753
|
<div class="event-detail-label">Description</div>
|
|
5368
4754
|
<textarea
|
|
5369
4755
|
class="event-detail-description-input"
|
|
5370
|
-
.value=${
|
|
4756
|
+
.value=${eventDescription}
|
|
5371
4757
|
placeholder="Add description..."
|
|
5372
4758
|
rows="3"
|
|
5373
4759
|
@input=${(e: Event) => {
|
|
@@ -5397,7 +4783,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
5397
4783
|
clearTimeout(this.updateEventTimeout);
|
|
5398
4784
|
this.updateEventTimeout = null;
|
|
5399
4785
|
}
|
|
5400
|
-
if (newDescription !==
|
|
4786
|
+
if (newDescription !== eventDescription) {
|
|
5401
4787
|
this.dispatchEvent(
|
|
5402
4788
|
new CustomEvent("update-event", {
|
|
5403
4789
|
detail: {
|
|
@@ -5635,18 +5021,3 @@ export class CalendarViewElement extends LitElement {
|
|
|
5635
5021
|
`;
|
|
5636
5022
|
}
|
|
5637
5023
|
}
|
|
5638
|
-
|
|
5639
|
-
// Helper to get day index (0-6) within a week for a given date
|
|
5640
|
-
function getDayIndexInWeek(week: WeekInfo, date: Date) {
|
|
5641
|
-
const dateStart = new Date(date).setHours(0, 0, 0, 0);
|
|
5642
|
-
for (let i = 0; i < 7; i++) {
|
|
5643
|
-
const weekDay = week.days[i];
|
|
5644
|
-
if (weekDay && new Date(weekDay).setHours(0, 0, 0, 0) === dateStart) {
|
|
5645
|
-
return i;
|
|
5646
|
-
}
|
|
5647
|
-
}
|
|
5648
|
-
// Date is before or after this week
|
|
5649
|
-
const weekStart = new Date(week.days[0]!).setHours(0, 0, 0, 0);
|
|
5650
|
-
if (dateStart < weekStart) return 0;
|
|
5651
|
-
return 6;
|
|
5652
|
-
}
|