@luckydye/calendar 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,16 @@ import type {
3
3
  EventSegment,
4
4
  WeekInfo,
5
5
  } from "../CalendarInternal.js";
6
- import { TIME_SCALE_DAY_HEIGHT, type CalendarLayer, type LayerContext } from "../CalendarLayer.js";
6
+ import type { CalendarLayer, LayerContext } from "../CalendarLayer.js";
7
7
  import { hexToRgb, rgbToHsl } from "../Color.js";
8
8
 
9
9
  const MIN_EVENT_HEIGHT = 20;
10
+ const COMPACT_EVENT_HEIGHT = 6;
11
+ const COMPACT_EVENT_THRESHOLD = 110;
12
+ const TIME_SCALE_EVENT_THRESHOLD = 400;
13
+ const MONTH_LABEL_HEIGHT = 40;
14
+ const MONTH_LABEL_GAP = 4;
15
+ const DAY_CELL_EVENT_INSET = 6;
10
16
 
11
17
  export interface EventRect {
12
18
  event: CalendarEvent;
@@ -16,8 +22,22 @@ export interface EventRect {
16
22
  height: number;
17
23
  }
18
24
 
25
+ export interface PreviewEventData {
26
+ id: string;
27
+ start: Date;
28
+ end: Date;
29
+ color: {
30
+ fill: string;
31
+ stroke: string;
32
+ text: string;
33
+ dashed?: boolean;
34
+ };
35
+ excludeEventId?: string | null;
36
+ }
37
+
19
38
  export interface EventsState {
20
39
  events: CalendarEvent[];
40
+ previewEvents: PreviewEventData[];
21
41
  hoveredEventId: string | null;
22
42
  isEventSelected: (event: CalendarEvent) => boolean;
23
43
  shouldRenderEventWithStripes: (event: CalendarEvent) => boolean;
@@ -37,6 +57,7 @@ export function createEventsLayer(
37
57
  width,
38
58
  height,
39
59
  scrollTop,
60
+ scrollLeft,
40
61
  dayWidth,
41
62
  dayHeight,
42
63
  leftGutterWidth,
@@ -47,25 +68,42 @@ export function createEventsLayer(
47
68
  getDayVisualPosition,
48
69
  } = lc;
49
70
 
50
- const events = state.events;
71
+ const previewEvents = state.previewEvents;
72
+ const excludedEventIds = new Set(
73
+ previewEvents
74
+ .map((preview) => preview.excludeEventId)
75
+ .filter((id): id is string => Boolean(id)),
76
+ );
77
+ const events = state.events.filter(
78
+ (event) => !excludedEventIds.has(event.id),
79
+ );
51
80
  const viewportBottom = scrollTop + height;
52
- const showTimeScale = dayHeight >= TIME_SCALE_DAY_HEIGHT;
81
+ const showTimeScale = dayHeight >= TIME_SCALE_EVENT_THRESHOLD;
82
+ const showCompactEvents =
83
+ !showTimeScale && dayHeight < COMPACT_EVENT_THRESHOLD;
53
84
 
54
85
  layer.eventRects = [];
55
86
 
56
87
  if (visibleWeeks.length === 0) return;
57
88
 
58
89
  const firstVisibleWeek = visibleWeeks[0]!;
59
- const lastVisibleWeek =
60
- visibleWeeks[visibleWeeks.length - 1]!;
90
+ const lastVisibleWeek = visibleWeeks[visibleWeeks.length - 1]!;
61
91
  const firstVisibleDay = firstVisibleWeek.days[0]!;
62
92
  const lastVisibleDay = lastVisibleWeek.days[6]!;
63
93
  const visibleStartTime = firstVisibleDay.getTime();
64
- const visibleEndTime =
65
- lastVisibleDay.getTime() + 86400000 - 1;
94
+ const visibleEndTime = lastVisibleDay.getTime() + 86400000 - 1;
66
95
 
67
96
  const dayOccupiedRows = new Map<string, Set<number>>();
68
97
  const eventRowIndex = new Map<string, number>();
98
+ const overflowedDays = new Map<
99
+ string,
100
+ {
101
+ weekIndex: number;
102
+ dayIndex: number;
103
+ topInset: number;
104
+ maxEventsInRow: number;
105
+ }
106
+ >();
69
107
 
70
108
  const segments: EventSegment[] = [];
71
109
  const allDaySegments: EventSegment[] = [];
@@ -76,10 +114,7 @@ export function createEventsLayer(
76
114
  const eventStartTime = event.start.getTime();
77
115
  const eventEndTime = event.end.getTime();
78
116
 
79
- if (
80
- eventEndTime < visibleStartTime ||
81
- eventStartTime > visibleEndTime
82
- )
117
+ if (eventEndTime < visibleStartTime || eventStartTime > visibleEndTime)
83
118
  continue;
84
119
 
85
120
  const eventWeeks: {
@@ -95,10 +130,7 @@ export function createEventsLayer(
95
130
  const weekStart = weekStartDay.getTime();
96
131
  const weekEnd = weekEndDay.getTime() + 86399999;
97
132
 
98
- if (
99
- eventEndTime >= weekStart &&
100
- eventStartTime <= weekEnd
101
- ) {
133
+ if (eventEndTime >= weekStart && eventStartTime <= weekEnd) {
102
134
  const weekIndex = allWeeks.indexOf(week);
103
135
  eventWeeks.push({ weekIndex, week });
104
136
  }
@@ -117,19 +149,13 @@ export function createEventsLayer(
117
149
  let endDayIndex = 6;
118
150
 
119
151
  if (isStart) {
120
- startDayIndex = getDayIndexInWeek(
121
- week,
122
- event.start,
123
- );
152
+ startDayIndex = getDayIndexInWeek(week, event.start);
124
153
  }
125
154
  if (isEnd) {
126
155
  const effectiveEnd = isAllDay
127
156
  ? new Date(event.end.getTime() - 1)
128
157
  : event.end;
129
- endDayIndex = getDayIndexInWeek(
130
- week,
131
- effectiveEnd,
132
- );
158
+ endDayIndex = getDayIndexInWeek(week, effectiveEnd);
133
159
  }
134
160
 
135
161
  const segment = {
@@ -153,30 +179,47 @@ export function createEventsLayer(
153
179
 
154
180
  const renderedTimedSegments = showTimeScale
155
181
  ? timedSegments.flatMap((seg) => {
156
- if (seg.startDayIndex === seg.endDayIndex)
157
- return [seg];
182
+ if (seg.startDayIndex === seg.endDayIndex) return [seg];
158
183
  const result = [];
159
- for (
160
- let d = seg.startDayIndex;
161
- d <= seg.endDayIndex;
162
- d++
163
- ) {
184
+ for (let d = seg.startDayIndex; d <= seg.endDayIndex; d++) {
164
185
  result.push({
165
186
  ...seg,
166
187
  startDayIndex: d,
167
188
  endDayIndex: d,
168
- isStart:
169
- d === seg.startDayIndex && seg.isStart,
189
+ isStart: d === seg.startDayIndex && seg.isStart,
170
190
  isEnd: d === seg.endDayIndex && seg.isEnd,
171
191
  totalWeeks: 1,
172
192
  });
173
193
  }
174
194
  return result;
175
- })
195
+ })
176
196
  : timedSegments;
177
197
 
178
198
  segments.push(...allDaySegments, ...renderedTimedSegments);
179
199
 
200
+ const monthHeaderWeeks = new Set<number>();
201
+ const firstVisibleWeekIndex = allWeeks.indexOf(visibleWeeks[0]!);
202
+ let previousMonthKey: string | null = null;
203
+ if (firstVisibleWeekIndex > 0) {
204
+ const previousDay = allWeeks[firstVisibleWeekIndex - 1]?.days[6];
205
+ if (previousDay) {
206
+ previousMonthKey = `${previousDay.getMonth()}-${previousDay.getFullYear()}`;
207
+ }
208
+ }
209
+ for (const week of visibleWeeks) {
210
+ const weekIndex = allWeeks.indexOf(week);
211
+ if (weekIndex < 0) continue;
212
+ for (const day of week.days) {
213
+ if (!day) continue;
214
+ const monthKey = `${day.getMonth()}-${day.getFullYear()}`;
215
+ if (monthKey !== previousMonthKey) {
216
+ monthHeaderWeeks.add(weekIndex);
217
+ previousMonthKey = monthKey;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+
180
223
  // Calculate columns for overlapping events in time scale mode
181
224
  interface EventLayout {
182
225
  segment: EventSegment;
@@ -186,10 +229,7 @@ export function createEventsLayer(
186
229
  totalColumns: number;
187
230
  }
188
231
 
189
- const segmentsByWeekDay = new Map<
190
- string,
191
- EventSegment[]
192
- >();
232
+ const segmentsByWeekDay = new Map<string, EventSegment[]>();
193
233
  for (const segment of segments) {
194
234
  const allDay = segment.event.isAllDay === true;
195
235
  if (showTimeScale && !allDay) {
@@ -202,7 +242,7 @@ export function createEventsLayer(
202
242
  if (!segmentsByWeekDay.has(key)) {
203
243
  segmentsByWeekDay.set(key, []);
204
244
  }
205
- segmentsByWeekDay.get(key)!.push(segment);
245
+ segmentsByWeekDay.get(key)?.push(segment);
206
246
  }
207
247
  }
208
248
  }
@@ -214,8 +254,8 @@ export function createEventsLayer(
214
254
 
215
255
  for (const [dayKey, daySegments] of segmentsByWeekDay) {
216
256
  const [weekIndexStr, dayIndexStr] = dayKey.split("-");
217
- const dayIndex = parseInt(dayIndexStr!);
218
- const weekIndex = parseInt(weekIndexStr!);
257
+ const dayIndex = Number.parseInt(dayIndexStr!);
258
+ const weekIndex = Number.parseInt(weekIndexStr!);
219
259
 
220
260
  interface SegmentTime {
221
261
  segment: EventSegment;
@@ -228,34 +268,31 @@ export function createEventsLayer(
228
268
  const week = allWeeks[weekIndex];
229
269
  if (!week) continue;
230
270
 
231
- const dayStartTime = new Date(
232
- week.days[dayIndex]!,
233
- ).setHours(0, 0, 0, 0);
234
- const dayEndTime = new Date(
235
- week.days[dayIndex]!,
236
- ).setHours(23, 59, 59, 999);
237
- const eventStartTime = seg.event.start.getTime();
238
- const eventEndTime =
239
- seg.event.end.getTime() - 60000;
240
-
241
- const effectiveStartTime = Math.max(
242
- eventStartTime,
243
- dayStartTime,
271
+ const dayStartTime = new Date(week.days[dayIndex]!).setHours(
272
+ 0,
273
+ 0,
274
+ 0,
275
+ 0,
244
276
  );
245
- const effectiveEndTime = Math.min(
246
- eventEndTime,
247
- dayEndTime,
277
+ const dayEndTime = new Date(week.days[dayIndex]!).setHours(
278
+ 23,
279
+ 59,
280
+ 59,
281
+ 999,
248
282
  );
283
+ const eventStartTime = seg.event.start.getTime();
284
+ const eventEndTime = seg.event.end.getTime() - 60000;
285
+
286
+ const effectiveStartTime = Math.max(eventStartTime, dayStartTime);
287
+ const effectiveEndTime = Math.min(eventEndTime, dayEndTime);
249
288
 
250
289
  const effectiveStart = new Date(effectiveStartTime);
251
290
  const effectiveEnd = new Date(effectiveEndTime);
252
291
 
253
292
  const startMinutes =
254
- effectiveStart.getHours() * 60 +
255
- effectiveStart.getMinutes();
293
+ effectiveStart.getHours() * 60 + effectiveStart.getMinutes();
256
294
  const endMinutes =
257
- effectiveEnd.getHours() * 60 +
258
- effectiveEnd.getMinutes();
295
+ effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
259
296
 
260
297
  segmentTimes.push({
261
298
  segment: seg,
@@ -268,9 +305,7 @@ export function createEventsLayer(
268
305
  if (a.startMinutes !== b.startMinutes)
269
306
  return a.startMinutes - b.startMinutes;
270
307
  return (
271
- b.endMinutes -
272
- b.startMinutes -
273
- (a.endMinutes - a.startMinutes)
308
+ b.endMinutes - b.startMinutes - (a.endMinutes - a.startMinutes)
274
309
  );
275
310
  });
276
311
 
@@ -278,15 +313,8 @@ export function createEventsLayer(
278
313
 
279
314
  for (const st of segmentTimes) {
280
315
  let columnIndex = 0;
281
- for (
282
- ;
283
- columnIndex < columns.length;
284
- columnIndex++
285
- ) {
286
- if (
287
- columns[columnIndex]!.endMinutes <=
288
- st.startMinutes
289
- ) {
316
+ for (; columnIndex < columns.length; columnIndex++) {
317
+ if (columns[columnIndex]?.endMinutes <= st.startMinutes) {
290
318
  break;
291
319
  }
292
320
  }
@@ -294,8 +322,7 @@ export function createEventsLayer(
294
322
  if (columnIndex === columns.length) {
295
323
  columns.push({ endMinutes: st.endMinutes });
296
324
  } else {
297
- columns[columnIndex]!.endMinutes =
298
- st.endMinutes;
325
+ columns[columnIndex]!.endMinutes = st.endMinutes;
299
326
  }
300
327
 
301
328
  const segmentKey = `${st.segment.weekIndex}-${st.segment.event.id}-${dayIndex}`;
@@ -329,55 +356,52 @@ export function createEventsLayer(
329
356
  const weekYOffset = week.yOffset;
330
357
  const allDay = event.isAllDay === true;
331
358
 
332
- const startVisualPos =
333
- getDayVisualPosition(startDayIndex);
359
+ const startVisualPos = getDayVisualPosition(startDayIndex);
334
360
  const endVisualPos = getDayVisualPosition(endDayIndex);
335
361
 
336
362
  let yStart: number;
337
363
  let yEnd: number;
338
364
 
339
365
  if (showTimeScale && !allDay) {
340
- const dayStartTime = new Date(
341
- week.days[startDayIndex]!,
342
- ).setHours(0, 0, 0, 0);
343
- const dayEndTime = new Date(
344
- week.days[endDayIndex]!,
345
- ).setHours(23, 59, 59, 999);
346
- const eventStartTime = event.start.getTime();
347
- const eventEndTime =
348
- event.end.getTime() - 60000;
349
-
350
- const effectiveStartTime = Math.max(
351
- eventStartTime,
352
- dayStartTime,
366
+ const dayStartTime = new Date(week.days[startDayIndex]!).setHours(
367
+ 0,
368
+ 0,
369
+ 0,
370
+ 0,
353
371
  );
354
- const effectiveEndTime = Math.min(
355
- eventEndTime,
356
- dayEndTime,
372
+ const dayEndTime = new Date(week.days[endDayIndex]!).setHours(
373
+ 23,
374
+ 59,
375
+ 59,
376
+ 999,
357
377
  );
378
+ const eventStartTime = event.start.getTime();
379
+ const eventEndTime = event.end.getTime() - 60000;
358
380
 
359
- const effectiveStart = new Date(
360
- effectiveStartTime,
361
- );
381
+ const effectiveStartTime = Math.max(eventStartTime, dayStartTime);
382
+ const effectiveEndTime = Math.min(eventEndTime, dayEndTime);
383
+
384
+ const effectiveStart = new Date(effectiveStartTime);
362
385
  const effectiveEnd = new Date(effectiveEndTime);
363
386
 
364
387
  const startMinutes =
365
- effectiveStart.getHours() * 60 +
366
- effectiveStart.getMinutes();
388
+ effectiveStart.getHours() * 60 + effectiveStart.getMinutes();
367
389
  const endMinutes =
368
- effectiveEnd.getHours() * 60 +
369
- effectiveEnd.getMinutes();
390
+ effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
370
391
 
371
- const visualRowY =
372
- weekYOffset +
373
- startVisualPos.row * dayHeight;
374
- yStart =
375
- visualRowY +
376
- (startMinutes / 1440) * dayHeight;
377
- yEnd =
378
- visualRowY +
379
- (endMinutes / 1440) * dayHeight;
392
+ const visualRowY = weekYOffset + startVisualPos.row * dayHeight;
393
+ yStart = visualRowY + (startMinutes / 1440) * dayHeight;
394
+ yEnd = visualRowY + (endMinutes / 1440) * dayHeight;
380
395
  } else {
396
+ const topInset = monthHeaderWeeks.has(weekIndex)
397
+ ? MONTH_LABEL_HEIGHT + MONTH_LABEL_GAP + DAY_CELL_EVENT_INSET
398
+ : DAY_CELL_EVENT_INSET;
399
+ const rowHeight = showCompactEvents
400
+ ? COMPACT_EVENT_HEIGHT + 2
401
+ : MIN_EVENT_HEIGHT + 2;
402
+ const eventBlockHeight = showCompactEvents
403
+ ? COMPACT_EVENT_HEIGHT
404
+ : MIN_EVENT_HEIGHT;
381
405
  const eventKey = `${weekIndex}-${event.id}`;
382
406
  let rowIndex = eventRowIndex.get(eventKey);
383
407
 
@@ -385,14 +409,9 @@ export function createEventsLayer(
385
409
  rowIndex = 0;
386
410
  while (true) {
387
411
  let rowFree = true;
388
- for (
389
- let d = startDayIndex;
390
- d <= endDayIndex;
391
- d++
392
- ) {
412
+ for (let d = startDayIndex; d <= endDayIndex; d++) {
393
413
  const dayKey = `${weekIndex}-${d}`;
394
- const occupied =
395
- dayOccupiedRows.get(dayKey);
414
+ const occupied = dayOccupiedRows.get(dayKey);
396
415
  if (occupied?.has(rowIndex)) {
397
416
  rowFree = false;
398
417
  break;
@@ -404,11 +423,7 @@ export function createEventsLayer(
404
423
  eventRowIndex.set(eventKey, rowIndex);
405
424
  }
406
425
 
407
- for (
408
- let d = startDayIndex;
409
- d <= endDayIndex;
410
- d++
411
- ) {
426
+ for (let d = startDayIndex; d <= endDayIndex; d++) {
412
427
  const dayKey = `${weekIndex}-${d}`;
413
428
  let occupied = dayOccupiedRows.get(dayKey);
414
429
  if (!occupied) {
@@ -418,23 +433,31 @@ export function createEventsLayer(
418
433
  occupied.add(rowIndex);
419
434
  }
420
435
 
421
- const maxEventsInRow = Math.floor(
422
- (dayHeight - 4) / (MIN_EVENT_HEIGHT + 2),
423
- );
424
- if (rowIndex >= maxEventsInRow) continue;
436
+ const maxEventsInRow = Math.floor((dayHeight - topInset) / rowHeight);
437
+ const visibleEventsInRow = Math.max(0, maxEventsInRow - 1);
438
+ if (rowIndex >= visibleEventsInRow) {
439
+ for (let d = startDayIndex; d <= endDayIndex; d++) {
440
+ overflowedDays.set(`${weekIndex}-${d}`, {
441
+ weekIndex,
442
+ dayIndex: d,
443
+ topInset,
444
+ maxEventsInRow: visibleEventsInRow,
445
+ });
446
+ }
447
+ continue;
448
+ }
425
449
 
426
- const visualRowY =
427
- weekYOffset +
428
- startVisualPos.row * dayHeight;
429
- yStart =
430
- visualRowY +
431
- 4 +
432
- rowIndex * (MIN_EVENT_HEIGHT + 2);
433
- yEnd = yStart + MIN_EVENT_HEIGHT;
450
+ const visualRowY = weekYOffset + startVisualPos.row * dayHeight;
451
+ yStart = visualRowY + topInset + rowIndex * rowHeight;
452
+ yEnd = yStart + eventBlockHeight;
434
453
  }
435
454
 
436
455
  const eventHeight = Math.max(
437
- showTimeScale ? 4 : MIN_EVENT_HEIGHT,
456
+ showTimeScale
457
+ ? 4
458
+ : showCompactEvents
459
+ ? COMPACT_EVENT_HEIGHT
460
+ : MIN_EVENT_HEIGHT,
438
461
  yEnd - yStart,
439
462
  );
440
463
 
@@ -450,36 +473,28 @@ export function createEventsLayer(
450
473
  columnLayout &&
451
474
  columnLayout.totalColumns > 1
452
475
  ) {
453
- const columnWidth =
454
- dayWidth / columnLayout.totalColumns;
476
+ const columnWidth = dayWidth / columnLayout.totalColumns;
455
477
  x =
456
478
  leftGutterWidth +
457
- startVisualPos.col * dayWidth +
479
+ startVisualPos.col * dayWidth -
480
+ scrollLeft +
458
481
  columnLayout.column * columnWidth;
459
482
  spanWidth = columnWidth;
460
483
  } else {
461
- const colSpan =
462
- endVisualPos.col - startVisualPos.col + 1;
463
- x =
464
- leftGutterWidth +
465
- startVisualPos.col * dayWidth;
484
+ const colSpan = endVisualPos.col - startVisualPos.col + 1;
485
+ x = leftGutterWidth + startVisualPos.col * dayWidth - scrollLeft;
466
486
  spanWidth = colSpan * dayWidth;
467
487
  }
468
488
 
469
489
  const viewportY = yStart - scrollTop;
470
490
 
471
- if (
472
- viewportY + eventHeight < 0 ||
473
- viewportY > height
474
- )
475
- continue;
491
+ if (viewportY + eventHeight < 0 || viewportY > height) continue;
476
492
 
477
493
  const eventColor = event.color || "#888888";
478
494
  const isSelected = state.isEventSelected(event);
479
- const isHovered =
480
- state.hoveredEventId === event.id;
495
+ const isHovered = state.hoveredEventId === event.id;
481
496
 
482
- const padding = 2;
497
+ const padding = DAY_CELL_EVENT_INSET;
483
498
  const eventWidth = spanWidth - padding * 2;
484
499
  const leftBorderWidth = 3;
485
500
  const contentPadding = 6;
@@ -512,65 +527,52 @@ export function createEventsLayer(
512
527
 
513
528
  const hsl = rgbToHsl(hexToRgb(eventColor));
514
529
  const textPrimary =
515
- styles["--text-primary"] ||
516
- "rgba(255, 255, 255, 0.9)";
517
- const textInverse =
518
- styles["--text-inverse"] || "rgb(0, 0, 0)";
530
+ styles["--text-primary"] || "rgba(255, 255, 255, 0.9)";
531
+ const textInverse = styles["--text-inverse"] || "rgb(0, 0, 0)";
519
532
  const backgroundColor = isSelected
520
533
  ? eventColor
521
- : `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(hsl[2] + 15, 40)}%, 0.45)`;
534
+ : `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
535
+ hsl[2] + 15,
536
+ 40,
537
+ )}%, 0.45)`;
522
538
  const borderColor = isSelected
523
539
  ? eventColor
524
- : `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(hsl[2] + 10, 70)}%, 1)`;
540
+ : `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
541
+ hsl[2] + 10,
542
+ 70,
543
+ )}%, 1)`;
525
544
 
526
- const useStripes =
527
- state.shouldRenderEventWithStripes(event);
545
+ const useStripes = state.shouldRenderEventWithStripes(event);
528
546
 
529
547
  ctx.beginPath();
530
- ctx.roundRect(
531
- x + padding,
532
- viewportY,
533
- eventWidth,
534
- eventHeight,
535
- [
536
- radiusTopLeft,
537
- radiusTopRight,
538
- radiusBottomRight,
539
- radiusBottomLeft,
540
- ],
541
- );
548
+ ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
549
+ radiusTopLeft,
550
+ radiusTopRight,
551
+ radiusBottomRight,
552
+ radiusBottomLeft,
553
+ ]);
542
554
 
543
555
  ctx.fillStyle = backgroundColor;
544
556
  ctx.fill();
545
557
 
546
558
  if (useStripes) {
547
- const patternCanvas =
548
- state.getStripePatternCanvas();
559
+ const patternCanvas = state.getStripePatternCanvas();
549
560
  if (patternCanvas) {
550
561
  ctx.save();
551
562
  ctx.beginPath();
552
- ctx.roundRect(
553
- x + padding,
554
- viewportY,
555
- eventWidth,
556
- eventHeight,
557
- [
558
- radiusTopLeft,
559
- radiusTopRight,
560
- radiusBottomRight,
561
- radiusBottomLeft,
562
- ],
563
- );
563
+ ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
564
+ radiusTopLeft,
565
+ radiusTopRight,
566
+ radiusBottomRight,
567
+ radiusBottomLeft,
568
+ ]);
564
569
  ctx.clip();
565
570
 
566
571
  const patternSize = 12;
567
572
  const startX =
568
- Math.floor((x + padding) / patternSize) *
569
- patternSize;
573
+ Math.floor((x + padding) / patternSize) * patternSize;
570
574
  const startY =
571
- Math.floor(yStart / patternSize) *
572
- patternSize -
573
- scrollTop;
575
+ Math.floor(yStart / patternSize) * patternSize - scrollTop;
574
576
 
575
577
  for (
576
578
  let py = startY;
@@ -582,13 +584,7 @@ export function createEventsLayer(
582
584
  px < x + padding + eventWidth;
583
585
  px += patternSize
584
586
  ) {
585
- ctx.drawImage(
586
- patternCanvas,
587
- px,
588
- py,
589
- patternSize,
590
- patternSize,
591
- );
587
+ ctx.drawImage(patternCanvas, px, py, patternSize, patternSize);
592
588
  }
593
589
  }
594
590
 
@@ -604,12 +600,7 @@ export function createEventsLayer(
604
600
  viewportY + 2,
605
601
  leftBorderWidth,
606
602
  eventHeight - 4,
607
- [
608
- radiusTopLeft,
609
- radiusTopLeft,
610
- radiusBottomLeft,
611
- radiusBottomLeft,
612
- ],
603
+ [radiusTopLeft, radiusTopLeft, radiusBottomLeft, radiusBottomLeft],
613
604
  );
614
605
  ctx.fill();
615
606
  }
@@ -618,43 +609,30 @@ export function createEventsLayer(
618
609
  ctx.strokeStyle = textInverse;
619
610
  ctx.lineWidth = 1;
620
611
  ctx.beginPath();
621
- ctx.roundRect(
622
- x + padding,
623
- viewportY,
624
- eventWidth,
625
- eventHeight,
626
- [
627
- radiusTopLeft,
628
- radiusTopRight,
629
- radiusBottomRight,
630
- radiusBottomLeft,
631
- ],
632
- );
612
+ ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
613
+ radiusTopLeft,
614
+ radiusTopRight,
615
+ radiusBottomRight,
616
+ radiusBottomLeft,
617
+ ]);
633
618
  ctx.stroke();
634
619
  }
635
620
 
636
621
  if (isHovered && !isSelected) {
637
622
  ctx.strokeStyle =
638
- styles["--grid-color-hover"] ||
639
- "rgba(255, 255, 255, 0.2)";
623
+ styles["--grid-color-hover"] || "rgba(255, 255, 255, 0.2)";
640
624
  ctx.lineWidth = 1;
641
625
  ctx.beginPath();
642
- ctx.roundRect(
643
- x + padding,
644
- viewportY,
645
- eventWidth,
646
- eventHeight,
647
- [
648
- radiusTopLeft,
649
- radiusTopRight,
650
- radiusBottomRight,
651
- radiusBottomLeft,
652
- ],
653
- );
626
+ ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
627
+ radiusTopLeft,
628
+ radiusTopRight,
629
+ radiusBottomRight,
630
+ radiusBottomLeft,
631
+ ]);
654
632
  ctx.stroke();
655
633
  }
656
634
 
657
- if (isStart && eventHeight >= 16) {
635
+ if (!showCompactEvents && isStart && eventHeight >= 16) {
658
636
  const textColor = isSelected
659
637
  ? textInverse || "white"
660
638
  : textPrimary || eventColor;
@@ -663,18 +641,9 @@ export function createEventsLayer(
663
641
  ctx.textAlign = "left";
664
642
  ctx.textBaseline = "top";
665
643
 
666
- const textX =
667
- x +
668
- padding +
669
- leftBorderWidth +
670
- contentPadding +
671
- 1;
644
+ const textX = x + padding + leftBorderWidth + contentPadding + 1;
672
645
  const textY = viewportY + 6;
673
- let maxTextWidth =
674
- eventWidth -
675
- leftBorderWidth -
676
- contentPadding -
677
- 4;
646
+ let maxTextWidth = eventWidth - leftBorderWidth - contentPadding - 4;
678
647
 
679
648
  ctx.save();
680
649
  ctx.beginPath();
@@ -689,42 +658,29 @@ export function createEventsLayer(
689
658
  if (event.rrule) {
690
659
  ctx.font = `11px ${fontFamily}`;
691
660
  const recurIcon = "⟳";
692
- const iconWidth =
693
- ctx.measureText(recurIcon).width;
661
+ const iconWidth = ctx.measureText(recurIcon).width;
694
662
  ctx.fillText(recurIcon, textX, textY);
695
663
  maxTextWidth -= iconWidth + 4;
696
664
  }
697
665
 
698
666
  const titleStartX =
699
- textX +
700
- (event.rrule
701
- ? ctx.measureText("⟳").width + 4
702
- : 0);
667
+ textX + (event.rrule ? ctx.measureText("⟳").width + 4 : 0);
703
668
  let displayTitle = event.title;
704
669
  ctx.font = `11px ${fontFamily}`;
705
- const titleWidth =
706
- ctx.measureText(displayTitle).width;
670
+ const titleWidth = ctx.measureText(displayTitle).width;
707
671
 
708
672
  if (titleWidth > maxTextWidth) {
709
673
  const ellipsis = "…";
710
- const ellipsisWidth =
711
- ctx.measureText(ellipsis).width;
674
+ const ellipsisWidth = ctx.measureText(ellipsis).width;
712
675
 
713
676
  let left = 0;
714
677
  let right = displayTitle.length;
715
678
  let bestFit = 0;
716
679
 
717
680
  while (left <= right) {
718
- const mid = Math.floor(
719
- (left + right) / 2,
720
- );
721
- const testText = displayTitle.substring(
722
- 0,
723
- mid,
724
- );
725
- const testWidth =
726
- ctx.measureText(testText).width +
727
- ellipsisWidth;
681
+ const mid = Math.floor((left + right) / 2);
682
+ const testText = displayTitle.substring(0, mid);
683
+ const testWidth = ctx.measureText(testText).width + ellipsisWidth;
728
684
 
729
685
  if (testWidth <= maxTextWidth) {
730
686
  bestFit = mid;
@@ -734,31 +690,23 @@ export function createEventsLayer(
734
690
  }
735
691
  }
736
692
 
737
- displayTitle =
738
- displayTitle.substring(0, bestFit) +
739
- ellipsis;
693
+ displayTitle = displayTitle.substring(0, bestFit) + ellipsis;
740
694
  }
741
695
 
742
- ctx.fillText(
743
- displayTitle,
744
- titleStartX,
745
- textY,
746
- );
696
+ ctx.fillText(displayTitle, titleStartX, textY);
747
697
 
748
698
  if (eventHeight >= 32) {
749
699
  const formatTime = (date: Date) => {
750
700
  const hours = date.getHours();
751
701
  const minutes = date.getMinutes();
752
- const ampm =
753
- hours >= 12 ? "PM" : "AM";
754
- const displayHours =
755
- hours % 12 || 12;
756
- return `${displayHours}:${minutes.toString().padStart(2, "0")} ${ampm}`;
702
+ const ampm = hours >= 12 ? "PM" : "AM";
703
+ const displayHours = hours % 12 || 12;
704
+ return `${displayHours}:${minutes
705
+ .toString()
706
+ .padStart(2, "0")} ${ampm}`;
757
707
  };
758
708
 
759
- const startTime = formatTime(
760
- event.start,
761
- );
709
+ const startTime = formatTime(event.start);
762
710
  const endTime = formatTime(event.end);
763
711
  const timeText = `${startTime} – ${endTime}`;
764
712
 
@@ -769,27 +717,21 @@ export function createEventsLayer(
769
717
  ctx.font = `10px ${fontFamily}`;
770
718
 
771
719
  let displayTime = timeText;
772
- let timeWidth =
773
- ctx.measureText(displayTime).width;
720
+ const timeWidth = ctx.measureText(displayTime).width;
774
721
 
775
722
  if (timeWidth > maxTextWidth) {
776
723
  const ellipsis = "…";
777
- const ellipsisWidth =
778
- ctx.measureText(ellipsis).width;
724
+ const ellipsisWidth = ctx.measureText(ellipsis).width;
779
725
 
780
726
  let left = 0;
781
727
  let right = displayTime.length;
782
728
  let bestFit = 0;
783
729
 
784
730
  while (left <= right) {
785
- const mid = Math.floor(
786
- (left + right) / 2,
787
- );
788
- const testText =
789
- displayTime.substring(0, mid);
731
+ const mid = Math.floor((left + right) / 2);
732
+ const testText = displayTime.substring(0, mid);
790
733
  const testWidth =
791
- ctx.measureText(testText)
792
- .width + ellipsisWidth;
734
+ ctx.measureText(testText).width + ellipsisWidth;
793
735
 
794
736
  if (testWidth <= maxTextWidth) {
795
737
  bestFit = mid;
@@ -799,16 +741,10 @@ export function createEventsLayer(
799
741
  }
800
742
  }
801
743
 
802
- displayTime =
803
- displayTime.substring(0, bestFit) +
804
- ellipsis;
744
+ displayTime = displayTime.substring(0, bestFit) + ellipsis;
805
745
  }
806
746
 
807
- ctx.fillText(
808
- displayTime,
809
- textX,
810
- textY + 14,
811
- );
747
+ ctx.fillText(displayTime, textX, textY + 14);
812
748
  }
813
749
 
814
750
  ctx.restore();
@@ -825,130 +761,324 @@ export function createEventsLayer(
825
761
  });
826
762
  }
827
763
 
828
- // Render month labels
829
- const monthNames = [
830
- "January",
831
- "February",
832
- "March",
833
- "April",
834
- "May",
835
- "June",
836
- "July",
837
- "August",
838
- "September",
839
- "October",
840
- "November",
841
- "December",
842
- ];
843
-
844
- const monthBoundaries: {
845
- monthKey: string;
846
- monthName: string;
847
- year: number;
848
- yOffset: number;
849
- }[] = [];
850
- const seenMonths = new Set<string>();
764
+ if (!showTimeScale) {
765
+ const textMuted = styles["--text-muted"] || "rgba(255, 255, 255, 0.4)";
766
+ ctx.save();
767
+ ctx.font = `600 12px ${fontFamily}`;
768
+ ctx.textAlign = "left";
769
+ ctx.textBaseline = "top";
770
+ ctx.fillStyle = textMuted;
771
+
772
+ for (const overflow of overflowedDays.values()) {
773
+ if (overflow.maxEventsInRow <= 0) continue;
774
+ const week = allWeeks[overflow.weekIndex];
775
+ if (!week || week.height === 0) continue;
851
776
 
852
- for (const week of visibleWeeks) {
853
- const firstDay = week.days[0];
854
- if (!firstDay) continue;
855
-
856
- const monthIndex = firstDay.getMonth();
857
- const year = firstDay.getFullYear();
858
- const monthKey = `${monthIndex}-${year}`;
859
-
860
- if (!seenMonths.has(monthKey)) {
861
- seenMonths.add(monthKey);
862
- const monthName = monthNames[monthIndex];
863
- if (monthName) {
864
- monthBoundaries.push({
865
- monthKey,
866
- monthName,
867
- year,
868
- yOffset: week.yOffset,
869
- });
777
+ const { col } = getDayVisualPosition(overflow.dayIndex);
778
+ const visualRowY = week.yOffset;
779
+ const dotY =
780
+ visualRowY +
781
+ overflow.topInset +
782
+ overflow.maxEventsInRow *
783
+ (showCompactEvents
784
+ ? COMPACT_EVENT_HEIGHT + 2
785
+ : MIN_EVENT_HEIGHT + 2) -
786
+ scrollTop;
787
+ const dotX = leftGutterWidth + col * dayWidth - scrollLeft + 8;
788
+
789
+ if (dotY < 0 || dotY > height) continue;
790
+ if (
791
+ dotX + ctx.measureText("...").width >
792
+ leftGutterWidth + (col + 1) * dayWidth - scrollLeft - 8
793
+ ) {
794
+ continue;
870
795
  }
796
+ ctx.fillText("...", dotX, dotY);
871
797
  }
798
+
799
+ ctx.restore();
872
800
  }
873
801
 
874
- for (let i = 0; i < monthBoundaries.length; i++) {
875
- const month = monthBoundaries[i]!;
876
- const nextMonth = monthBoundaries[i + 1];
877
- const labelY = month.yOffset;
878
- const nextMonthY = nextMonth
879
- ? nextMonth.yOffset
880
- : Infinity;
802
+ if (previewEvents.length > 0) {
803
+ for (const preview of previewEvents) {
804
+ if (showTimeScale) {
805
+ renderTimedPreview(
806
+ ctx,
807
+ preview,
808
+ allWeeks,
809
+ scrollTop,
810
+ scrollLeft,
811
+ dayWidth,
812
+ dayHeight,
813
+ leftGutterWidth,
814
+ fontFamily,
815
+ styles,
816
+ );
817
+ continue;
818
+ }
881
819
 
882
- if (nextMonthY < scrollTop) continue;
883
- if (labelY > viewportBottom) break;
820
+ renderStackedPreview(
821
+ ctx,
822
+ preview,
823
+ allWeeks,
824
+ scrollTop,
825
+ scrollLeft,
826
+ dayWidth,
827
+ dayHeight,
828
+ leftGutterWidth,
829
+ showCompactEvents,
830
+ monthHeaderWeeks,
831
+ dayOccupiedRows,
832
+ );
833
+ }
834
+ }
835
+ },
836
+ };
837
+ return layer;
838
+ }
884
839
 
885
- const labelPadding = [12, 0, 0, 12];
840
+ function renderTimedPreview(
841
+ ctx: CanvasRenderingContext2D,
842
+ preview: PreviewEventData,
843
+ allWeeks: WeekInfo[],
844
+ scrollTop: number,
845
+ scrollLeft: number,
846
+ dayWidth: number,
847
+ dayHeight: number,
848
+ leftGutterWidth: number,
849
+ fontFamily: string,
850
+ styles: Record<string, string>,
851
+ ): void {
852
+ const drawBlock = (colX: number, top: number, bottom: number) => {
853
+ const bx = colX + DAY_CELL_EVENT_INSET;
854
+ const bw = dayWidth - DAY_CELL_EVENT_INSET * 2;
855
+ const bh = Math.max(4, bottom - top);
856
+ ctx.fillStyle = preview.color.fill;
857
+ ctx.beginPath();
858
+ ctx.roundRect(bx, top, bw, bh, 4);
859
+ ctx.fill();
860
+ ctx.strokeStyle = preview.color.stroke;
861
+ ctx.lineWidth = 1;
862
+ if (preview.color.dashed !== false) ctx.setLineDash([6, 3]);
863
+ ctx.stroke();
864
+ ctx.setLineDash([]);
865
+ };
886
866
 
887
- const stickyTop = Math.max(
888
- 0,
889
- scrollTop - labelY,
890
- );
891
- const maxStickyTop = nextMonthY - labelY - 24;
892
- const clampedStickyTop = Math.min(
893
- stickyTop,
894
- maxStickyTop,
895
- );
896
- const labelTopMargin = 32;
897
- const finalTop =
898
- labelY +
899
- clampedStickyTop -
900
- scrollTop +
901
- labelTopMargin;
867
+ const getTimeY = (day: Date, hours: number, minutes: number) => {
868
+ const week = allWeeks.find((w) =>
869
+ w.days.some((d) => d.toDateString() === day.toDateString()),
870
+ );
871
+ if (!week) return null;
872
+ const totalMinutes = hours * 60 + minutes;
873
+ return week.yOffset + (totalMinutes / 1440) * dayHeight - scrollTop;
874
+ };
902
875
 
903
- ctx.save();
904
- ctx.font = `bold 18px ${fontFamily}`;
905
- ctx.textAlign = "left";
906
- ctx.textBaseline = "top";
876
+ const getDayColumnX = (day: Date) => {
877
+ const week = allWeeks.find((w) =>
878
+ w.days.some((d) => d.toDateString() === day.toDateString()),
879
+ );
880
+ if (!week) return null;
881
+ const dayIndex = week.days.findIndex(
882
+ (d) => d.toDateString() === day.toDateString(),
883
+ );
884
+ if (dayIndex < 0) return null;
885
+ return leftGutterWidth + dayIndex * dayWidth - scrollLeft;
886
+ };
907
887
 
908
- const labelText = `${month.monthName} ${month.year}`;
909
- const textWidth =
910
- ctx.measureText(labelText).width;
911
- const leftMargin = 8;
912
- const textX =
913
- 64 + labelPadding[3] + leftMargin;
914
- const textY = finalTop + labelPadding[0];
915
-
916
- const bgPaddingLeft = 8;
917
- const bgPaddingRight = 8;
918
- const bgElevated =
919
- styles["--bg-elevated"] ||
920
- "rgba(0, 0, 0, 0.7)";
921
- ctx.fillStyle = bgElevated;
922
- ctx.beginPath();
923
- ctx.roundRect(
924
- textX - bgPaddingLeft,
925
- textY - 4,
926
- textWidth + bgPaddingLeft + bgPaddingRight,
927
- 26,
928
- 6,
929
- );
930
- ctx.fill();
888
+ const sameDay = preview.start.toDateString() === preview.end.toDateString();
889
+ if (sameDay) {
890
+ const colX = getDayColumnX(preview.start);
891
+ const top = getTimeY(
892
+ preview.start,
893
+ preview.start.getHours(),
894
+ preview.start.getMinutes(),
895
+ );
896
+ const bottom = getTimeY(
897
+ preview.end,
898
+ preview.end.getHours(),
899
+ preview.end.getMinutes(),
900
+ );
901
+ if (colX != null && top != null && bottom != null) {
902
+ drawBlock(colX, top, bottom);
903
+ }
904
+ } else {
905
+ const current = new Date(preview.start);
906
+ current.setHours(0, 0, 0, 0);
907
+ const endDay = new Date(preview.end);
908
+ endDay.setHours(23, 59, 59, 999);
909
+ while (current <= endDay) {
910
+ const colX = getDayColumnX(current);
911
+ if (colX != null) {
912
+ const isFirst = current.toDateString() === preview.start.toDateString();
913
+ const isLast = current.toDateString() === preview.end.toDateString();
914
+ let top: number | null;
915
+ let bottom: number | null;
916
+ if (isFirst) {
917
+ top = getTimeY(
918
+ current,
919
+ preview.start.getHours(),
920
+ preview.start.getMinutes(),
921
+ );
922
+ bottom = getTimeY(current, 23, 59);
923
+ } else if (isLast) {
924
+ top = getTimeY(current, 0, 0);
925
+ bottom = getTimeY(
926
+ current,
927
+ preview.end.getHours(),
928
+ preview.end.getMinutes(),
929
+ );
930
+ } else {
931
+ top = getTimeY(current, 0, 0);
932
+ bottom = getTimeY(current, 23, 59);
933
+ }
934
+ if (top != null && bottom != null) {
935
+ drawBlock(colX, top, bottom);
936
+ }
937
+ }
938
+ current.setDate(current.getDate() + 1);
939
+ }
940
+ }
931
941
 
932
- const monthTextPrimary =
933
- styles["--text-primary"] ||
934
- "rgba(255, 255, 255, 0.95)";
935
- ctx.fillStyle = monthTextPrimary;
936
- ctx.fillText(labelText, textX, textY);
937
- ctx.restore();
942
+ const fmtTime = (d: Date) =>
943
+ `${d.getHours().toString().padStart(2, "0")}:${d
944
+ .getMinutes()
945
+ .toString()
946
+ .padStart(2, "0")}`;
947
+ const fmtDate = (d: Date) => `${d.getDate()}.${d.getMonth() + 1}`;
948
+ const label = sameDay
949
+ ? `${fmtTime(preview.start)} – ${fmtTime(preview.end)}`
950
+ : `${fmtDate(preview.start)} ${fmtTime(preview.start)} – ${fmtDate(
951
+ preview.end,
952
+ )} ${fmtTime(preview.end)}`;
953
+ const durationMs = Math.abs(preview.end.getTime() - preview.start.getTime());
954
+ const durationMinutes = durationMs / (1000 * 60);
955
+ const isShortEvent = durationMinutes < 15;
956
+ const firstColX = getDayColumnX(preview.start);
957
+ const lastColX = getDayColumnX(preview.end);
958
+ const startY = getTimeY(
959
+ preview.start,
960
+ preview.start.getHours(),
961
+ preview.start.getMinutes(),
962
+ );
963
+ const endY = getTimeY(
964
+ preview.end,
965
+ preview.end.getHours(),
966
+ preview.end.getMinutes(),
967
+ );
968
+
969
+ if (firstColX != null && lastColX != null && startY != null && endY != null) {
970
+ ctx.font = `600 11px ${fontFamily}`;
971
+ ctx.fillStyle = preview.color.text;
972
+ ctx.textAlign = "left";
973
+ const labelColX = firstColX;
974
+
975
+ if (isShortEvent) {
976
+ const labelY = startY;
977
+ const labelBgY = labelY + 6;
978
+ const labelTextY = labelY + 10;
979
+ const textWidth = ctx.measureText(label).width;
980
+ const bgPaddingX = 6;
981
+ const bgElevated = styles["--bg-elevated"] || "rgba(0, 0, 0, 0.8)";
982
+ ctx.fillStyle = bgElevated;
983
+ ctx.beginPath();
984
+ ctx.roundRect(labelColX + 4, labelBgY, textWidth + bgPaddingX * 2, 16, 4);
985
+ ctx.fill();
986
+ ctx.fillStyle = preview.color.text;
987
+ ctx.textBaseline = "top";
988
+ ctx.fillText(label, labelColX + 4 + bgPaddingX, labelTextY);
989
+ } else {
990
+ ctx.textBaseline = "top";
991
+ ctx.fillText(label, labelColX + DAY_CELL_EVENT_INSET + 4, startY + 4);
992
+ }
993
+ }
994
+ }
995
+
996
+ function renderStackedPreview(
997
+ ctx: CanvasRenderingContext2D,
998
+ preview: PreviewEventData,
999
+ allWeeks: WeekInfo[],
1000
+ scrollTop: number,
1001
+ scrollLeft: number,
1002
+ dayWidth: number,
1003
+ dayHeight: number,
1004
+ leftGutterWidth: number,
1005
+ showCompactEvents: boolean,
1006
+ monthHeaderWeeks: Set<number>,
1007
+ dayOccupiedRows: Map<string, Set<number>>,
1008
+ ): void {
1009
+ const rowHeight = showCompactEvents
1010
+ ? COMPACT_EVENT_HEIGHT + 2
1011
+ : MIN_EVENT_HEIGHT + 2;
1012
+ const eventBlockHeight = showCompactEvents
1013
+ ? COMPACT_EVENT_HEIGHT
1014
+ : MIN_EVENT_HEIGHT;
1015
+
1016
+ for (const [weekIndex, week] of allWeeks.entries()) {
1017
+ if (week.height === 0) continue;
1018
+ const weekStart = week.days[0];
1019
+ const weekEnd = week.days[6];
1020
+ if (!weekStart || !weekEnd) continue;
1021
+ const weekStartMs = weekStart.getTime();
1022
+ const weekEndMs = weekEnd.getTime() + 86399999;
1023
+ if (
1024
+ preview.end.getTime() < weekStartMs ||
1025
+ preview.start.getTime() > weekEndMs
1026
+ ) {
1027
+ continue;
1028
+ }
1029
+
1030
+ let startDayIndex = 0;
1031
+ let endDayIndex = 6;
1032
+ if (preview.start.getTime() >= weekStartMs) {
1033
+ startDayIndex = getDayIndexInWeek(week, preview.start);
1034
+ }
1035
+ if (preview.end.getTime() <= weekEndMs) {
1036
+ endDayIndex = getDayIndexInWeek(week, preview.end);
1037
+ }
1038
+
1039
+ let rowIndex = 0;
1040
+ while (true) {
1041
+ let rowFree = true;
1042
+ for (let d = startDayIndex; d <= endDayIndex; d++) {
1043
+ const occupied = dayOccupiedRows.get(`${weekIndex}-${d}`);
1044
+ if (occupied?.has(rowIndex)) {
1045
+ rowFree = false;
1046
+ break;
1047
+ }
938
1048
  }
939
- },
940
- };
941
- return layer;
1049
+ if (rowFree) break;
1050
+ rowIndex++;
1051
+ }
1052
+
1053
+ const topInset = monthHeaderWeeks.has(weekIndex)
1054
+ ? MONTH_LABEL_HEIGHT + MONTH_LABEL_GAP + DAY_CELL_EVENT_INSET
1055
+ : DAY_CELL_EVENT_INSET;
1056
+ const x =
1057
+ leftGutterWidth +
1058
+ startDayIndex * dayWidth -
1059
+ scrollLeft +
1060
+ DAY_CELL_EVENT_INSET;
1061
+ const width =
1062
+ (endDayIndex - startDayIndex + 1) * dayWidth - DAY_CELL_EVENT_INSET * 2;
1063
+ const y = week.yOffset + topInset + rowIndex * rowHeight - scrollTop;
1064
+
1065
+ ctx.fillStyle = preview.color.fill;
1066
+ ctx.beginPath();
1067
+ ctx.roundRect(x, y, width, eventBlockHeight, 4);
1068
+ ctx.fill();
1069
+ ctx.strokeStyle = preview.color.stroke;
1070
+ ctx.lineWidth = 1;
1071
+ if (preview.color.dashed !== false) ctx.setLineDash([6, 3]);
1072
+ ctx.stroke();
1073
+ ctx.setLineDash([]);
1074
+ }
942
1075
  }
943
1076
 
944
1077
  function getDayIndexInWeek(week: WeekInfo, date: Date) {
945
1078
  const dateStart = new Date(date).setHours(0, 0, 0, 0);
946
1079
  for (let i = 0; i < 7; i++) {
947
1080
  const weekDay = week.days[i];
948
- if (
949
- weekDay &&
950
- new Date(weekDay).setHours(0, 0, 0, 0) === dateStart
951
- ) {
1081
+ if (weekDay && new Date(weekDay).setHours(0, 0, 0, 0) === dateStart) {
952
1082
  return i;
953
1083
  }
954
1084
  }