@intinyagroup/calendar 0.1.1-alpha.12 → 0.1.1-alpha.13
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/EventCalendar.svelte +272 -38
- package/dist/EventCalendar.svelte.d.ts +5 -0
- package/package.json +1 -1
|
@@ -58,18 +58,33 @@
|
|
|
58
58
|
onDateClick?: (date: Date) => void;
|
|
59
59
|
onAddEvent?: (newEvent: CalendarEvent) => void;
|
|
60
60
|
onEventReschedule?: (detail: { event: CalendarEvent; newStart: Date; newEnd: Date }) => void;
|
|
61
|
+
onEventResize?: (detail: { event: CalendarEvent; newStart: Date; newEnd: Date }) => void;
|
|
61
62
|
} = $props();
|
|
62
63
|
|
|
63
64
|
let draggedEventId = $state<string | null>(null);
|
|
65
|
+
let resizingEventId = $state<string | null>(null);
|
|
66
|
+
let resizeStartY = $state<number>(0);
|
|
67
|
+
let resizeStartDuration = $state<number>(0);
|
|
68
|
+
|
|
69
|
+
function isMultiDayOrAllDay(ev: CalendarEvent): boolean {
|
|
70
|
+
if (ev.allDay) return true;
|
|
71
|
+
const s = new Date(ev.start);
|
|
72
|
+
const e = new Date(ev.end);
|
|
73
|
+
return !isSameDay(s, e) || (e.getTime() - s.getTime() >= 24 * 60 * 60 * 1000);
|
|
74
|
+
}
|
|
64
75
|
|
|
65
|
-
function handleEventDrop(targetDate: Date, targetHour
|
|
76
|
+
function handleEventDrop(targetDate: Date, targetHour?: number) {
|
|
66
77
|
if (!draggedEventId) return;
|
|
67
78
|
const ev = events.find((e) => e.id === draggedEventId);
|
|
68
79
|
if (!ev) return;
|
|
69
80
|
|
|
70
81
|
const origDurationMs = new Date(ev.end).getTime() - new Date(ev.start).getTime();
|
|
71
82
|
const newStart = new Date(targetDate);
|
|
72
|
-
|
|
83
|
+
if (targetHour !== undefined) {
|
|
84
|
+
newStart.setHours(targetHour, 0, 0, 0);
|
|
85
|
+
} else {
|
|
86
|
+
newStart.setHours(new Date(ev.start).getHours(), new Date(ev.start).getMinutes(), 0, 0);
|
|
87
|
+
}
|
|
73
88
|
const newEnd = new Date(newStart.getTime() + origDurationMs);
|
|
74
89
|
|
|
75
90
|
events = events.map((e) =>
|
|
@@ -79,15 +94,115 @@
|
|
|
79
94
|
onEventReschedule?.({ event: ev, newStart, newEnd });
|
|
80
95
|
draggedEventId = null;
|
|
81
96
|
}
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
|
|
98
|
+
function handleResizeStart(e: MouseEvent, ev: CalendarEvent) {
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
resizingEventId = ev.id;
|
|
102
|
+
resizeStartY = e.clientY;
|
|
103
|
+
resizeStartDuration = new Date(ev.end).getTime() - new Date(ev.start).getTime();
|
|
104
|
+
|
|
105
|
+
function onMouseMove(moveEvent: MouseEvent) {
|
|
106
|
+
if (!resizingEventId) return;
|
|
107
|
+
const deltaY = moveEvent.clientY - resizeStartY;
|
|
108
|
+
// 56px per hour => deltaY / 56 hours
|
|
109
|
+
const deltaHours = deltaY / 56;
|
|
110
|
+
// Snap to 15 minute increments (0.25h = 15 * 60 * 1000 ms)
|
|
111
|
+
const snapIntervalMs = 15 * 60 * 1000;
|
|
112
|
+
const rawDeltaMs = deltaHours * 60 * 60 * 1000;
|
|
113
|
+
const snappedDeltaMs = Math.round(rawDeltaMs / snapIntervalMs) * snapIntervalMs;
|
|
114
|
+
const newDurationMs = Math.max(15 * 60 * 1000, resizeStartDuration + snappedDeltaMs);
|
|
115
|
+
|
|
116
|
+
events = events.map((item) => {
|
|
117
|
+
if (item.id === resizingEventId) {
|
|
118
|
+
const s = new Date(item.start);
|
|
119
|
+
return { ...item, end: new Date(s.getTime() + newDurationMs) };
|
|
120
|
+
}
|
|
121
|
+
return item;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onMouseUp() {
|
|
126
|
+
if (resizingEventId) {
|
|
127
|
+
const finished = events.find((item) => item.id === resizingEventId);
|
|
128
|
+
if (finished) {
|
|
129
|
+
onEventResize?.({ event: finished, newStart: new Date(finished.start), newEnd: new Date(finished.end) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
resizingEventId = null;
|
|
133
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
134
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
138
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type LayoutEvent = CalendarEvent & {
|
|
142
|
+
colIndex: number;
|
|
143
|
+
colTotal: number;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function computeOverlappingLayout(dayEvs: CalendarEvent[]): LayoutEvent[] {
|
|
147
|
+
// Sort events by start time, then longer events first
|
|
148
|
+
const sorted = [...dayEvs].sort((a, b) => {
|
|
149
|
+
const diff = new Date(a.start).getTime() - new Date(b.start).getTime();
|
|
150
|
+
if (diff !== 0) return diff;
|
|
151
|
+
return (new Date(b.end).getTime() - new Date(b.start).getTime()) - (new Date(a.end).getTime() - new Date(a.start).getTime());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const columns: CalendarEvent[][] = [];
|
|
155
|
+
const layoutMap = new Map<string, { colIndex: number }>();
|
|
156
|
+
|
|
157
|
+
for (const ev of sorted) {
|
|
158
|
+
let placed = false;
|
|
159
|
+
for (let i = 0; i < columns.length; i++) {
|
|
160
|
+
const lastInCol = columns[i][columns[i].length - 1];
|
|
161
|
+
if (new Date(lastInCol.end).getTime() <= new Date(ev.start).getTime()) {
|
|
162
|
+
columns[i].push(ev);
|
|
163
|
+
layoutMap.set(ev.id, { colIndex: i });
|
|
164
|
+
placed = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!placed) {
|
|
169
|
+
columns.push([ev]);
|
|
170
|
+
layoutMap.set(ev.id, { colIndex: columns.length - 1 });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Determine overlapping groups
|
|
175
|
+
return sorted.map((ev) => {
|
|
176
|
+
const colInfo = layoutMap.get(ev.id) || { colIndex: 0 };
|
|
177
|
+
// Count how many columns overlap with this event
|
|
178
|
+
let overlappingCols = 1;
|
|
179
|
+
for (let c = 0; c < columns.length; c++) {
|
|
180
|
+
const hasOverlap = columns[c].some(
|
|
181
|
+
(other) =>
|
|
182
|
+
new Date(other.start).getTime() < new Date(ev.end).getTime() &&
|
|
183
|
+
new Date(other.end).getTime() > new Date(ev.start).getTime()
|
|
184
|
+
);
|
|
185
|
+
if (hasOverlap && c + 1 > overlappingCols) {
|
|
186
|
+
overlappingCols = c + 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...ev,
|
|
192
|
+
colIndex: colInfo.colIndex,
|
|
193
|
+
colTotal: Math.max(1, overlappingCols),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
84
197
|
|
|
85
198
|
const activeTimeZone = $derived(
|
|
86
199
|
timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
87
200
|
);
|
|
88
201
|
|
|
89
|
-
const
|
|
202
|
+
const activeDateFnsLocale = $derived(locale.startsWith('id') ? localeId : localeEn);
|
|
203
|
+
const weekStartsOn = $derived(firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6);
|
|
90
204
|
|
|
205
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
91
206
|
// Day names localized and rotated according to firstDayOfWeek via date-fns
|
|
92
207
|
const dayNames = $derived.by(() => {
|
|
93
208
|
const start = startOfWeek(currentDate, { weekStartsOn });
|
|
@@ -137,7 +252,26 @@
|
|
|
137
252
|
});
|
|
138
253
|
|
|
139
254
|
function getEventsForDay(d: Date): CalendarEvent[] {
|
|
140
|
-
return events.filter((ev) =>
|
|
255
|
+
return events.filter((ev) => {
|
|
256
|
+
if (isSameDay(new Date(ev.start), d)) return true;
|
|
257
|
+
// Multi-day spanning: check if date falls between start and end
|
|
258
|
+
const s = new Date(ev.start);
|
|
259
|
+
const e = new Date(ev.end);
|
|
260
|
+
return d >= s && d <= e;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getTimedEventsForDay(d: Date): CalendarEvent[] {
|
|
265
|
+
return events.filter((ev) => !isMultiDayOrAllDay(ev) && isSameDay(new Date(ev.start), d));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getAllDayEventsForWeek(days: Date[]): CalendarEvent[] {
|
|
269
|
+
return events.filter((ev) => {
|
|
270
|
+
if (!isMultiDayOrAllDay(ev)) return false;
|
|
271
|
+
const s = new Date(ev.start);
|
|
272
|
+
const e = new Date(ev.end);
|
|
273
|
+
return days.some((d) => isSameDay(d, s) || isSameDay(d, e) || (d > s && d < e));
|
|
274
|
+
});
|
|
141
275
|
}
|
|
142
276
|
|
|
143
277
|
function getTimeInZone(date: Date): { hours: number; minutes: number } {
|
|
@@ -314,12 +448,18 @@
|
|
|
314
448
|
onDateClick?.(date);
|
|
315
449
|
openCreateModal(date);
|
|
316
450
|
}}
|
|
451
|
+
ondragover={(e) => e.preventDefault()}
|
|
452
|
+
ondrop={(e) => {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
e.stopPropagation();
|
|
455
|
+
handleEventDrop(date);
|
|
456
|
+
}}
|
|
317
457
|
class={cn(
|
|
318
|
-
'min-h-[
|
|
458
|
+
'min-h-[110px] p-1.5 flex flex-col gap-1 transition-colors hover:bg-[var(--ui-secondary)]/25 cursor-pointer select-none',
|
|
319
459
|
!currentMonth && 'bg-[var(--ui-muted)]/15 opacity-50'
|
|
320
460
|
)}
|
|
321
461
|
>
|
|
322
|
-
<div class="flex items-center justify-between mb-1">
|
|
462
|
+
<div class="flex items-center justify-between mb-0.5 px-1">
|
|
323
463
|
<span
|
|
324
464
|
class={cn(
|
|
325
465
|
'text-xs font-semibold size-6 flex items-center justify-center rounded-full',
|
|
@@ -335,27 +475,44 @@
|
|
|
335
475
|
{/if}
|
|
336
476
|
</div>
|
|
337
477
|
|
|
338
|
-
<!-- Event pills -->
|
|
478
|
+
<!-- Event pills with spanning indicator -->
|
|
339
479
|
<div class="flex flex-col gap-1 overflow-hidden">
|
|
340
480
|
{#each dayEvents.slice(0, 3) as ev (ev.id)}
|
|
481
|
+
{@const isSpanning = isMultiDayOrAllDay(ev)}
|
|
482
|
+
{@const isStartDay = isSameDay(new Date(ev.start), date)}
|
|
483
|
+
{@const isEndDay = isSameDay(new Date(ev.end), date)}
|
|
484
|
+
|
|
341
485
|
<button
|
|
342
486
|
type="button"
|
|
487
|
+
draggable="true"
|
|
488
|
+
ondragstart={(e) => {
|
|
489
|
+
e.stopPropagation();
|
|
490
|
+
draggedEventId = ev.id;
|
|
491
|
+
e.dataTransfer?.setData('text/plain', ev.id);
|
|
492
|
+
}}
|
|
343
493
|
onclick={(e) => {
|
|
344
494
|
e.stopPropagation();
|
|
345
495
|
onEventClick?.(ev);
|
|
346
496
|
}}
|
|
347
497
|
class={cn(
|
|
348
|
-
'truncate
|
|
349
|
-
|
|
498
|
+
'truncate px-1.5 py-0.5 text-left text-[11px] font-medium transition-opacity hover:opacity-85 text-white shadow-2xs cursor-grab active:cursor-grabbing',
|
|
499
|
+
isSpanning ? 'rounded-none' : 'rounded',
|
|
500
|
+
isSpanning && isStartDay && 'rounded-l-md',
|
|
501
|
+
isSpanning && isEndDay && 'rounded-r-md',
|
|
502
|
+
draggedEventId === ev.id ? 'opacity-40 ring-2 ring-white' : ''
|
|
350
503
|
)}
|
|
351
|
-
style=
|
|
504
|
+
style="background-color: {ev.color || 'var(--ui-primary)'};"
|
|
352
505
|
>
|
|
353
|
-
{
|
|
506
|
+
{#if isSpanning}
|
|
507
|
+
<span class="font-semibold">{ev.title}</span>
|
|
508
|
+
{:else}
|
|
509
|
+
<span class="opacity-80 font-normal">{formatTime(ev.start)}</span> {ev.title}
|
|
510
|
+
{/if}
|
|
354
511
|
</button>
|
|
355
512
|
{/each}
|
|
356
513
|
|
|
357
514
|
{#if dayEvents.length > 3}
|
|
358
|
-
<span class="text-[10px] font-semibold text-[var(--ui-muted-foreground)]
|
|
515
|
+
<span class="text-[10px] font-semibold text-[var(--ui-muted-foreground)] px-1">
|
|
359
516
|
+{dayEvents.length - 3} more
|
|
360
517
|
</span>
|
|
361
518
|
{/if}
|
|
@@ -377,6 +534,31 @@
|
|
|
377
534
|
{/each}
|
|
378
535
|
</div>
|
|
379
536
|
|
|
537
|
+
<!-- All-day header slot (FullCalendar 1:1) -->
|
|
538
|
+
{@const allDayEvents = getAllDayEventsForWeek(weekDays)}
|
|
539
|
+
{#if allDayEvents.length > 0}
|
|
540
|
+
<div class="grid grid-cols-8 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/10 text-xs divide-x divide-[var(--ui-border)] py-1.5">
|
|
541
|
+
<div class="col-span-1 text-right pr-2 text-[10px] uppercase tracking-wider font-semibold text-[var(--ui-muted-foreground)] flex items-center justify-end">
|
|
542
|
+
all-day
|
|
543
|
+
</div>
|
|
544
|
+
<div class="col-span-7 px-2 flex flex-col gap-1">
|
|
545
|
+
{#each allDayEvents as ev (ev.id)}
|
|
546
|
+
<button
|
|
547
|
+
type="button"
|
|
548
|
+
onclick={(e) => {
|
|
549
|
+
e.stopPropagation();
|
|
550
|
+
onEventClick?.(ev);
|
|
551
|
+
}}
|
|
552
|
+
class="w-full truncate rounded px-2 py-0.5 text-left text-[11px] font-semibold text-white shadow-2xs hover:opacity-90"
|
|
553
|
+
style="background-color: {ev.color || 'var(--ui-primary)'};"
|
|
554
|
+
>
|
|
555
|
+
{ev.title} {#if ev.description}• {ev.description}{/if}
|
|
556
|
+
</button>
|
|
557
|
+
{/each}
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
{/if}
|
|
561
|
+
|
|
380
562
|
<div class="grid grid-cols-8 divide-x divide-[var(--ui-border)] max-h-[600px] overflow-y-auto">
|
|
381
563
|
<!-- Hours Column -->
|
|
382
564
|
<div class="col-span-1 divide-y divide-[var(--ui-border)]/50 text-right pr-2 text-[11px] font-medium text-[var(--ui-muted-foreground)]">
|
|
@@ -387,7 +569,8 @@
|
|
|
387
569
|
|
|
388
570
|
<!-- 7 Days Grid Columns -->
|
|
389
571
|
{#each weekDays as d}
|
|
390
|
-
{@const dayEvents =
|
|
572
|
+
{@const dayEvents = getTimedEventsForDay(d)}
|
|
573
|
+
{@const layoutEvents = computeOverlappingLayout(dayEvents)}
|
|
391
574
|
{@const isCurrentDay = isToday(d)}
|
|
392
575
|
{@const nowTime = getTimeInZone(new Date())}
|
|
393
576
|
{@const nowMinutes = nowTime.hours * 60 + nowTime.minutes}
|
|
@@ -397,7 +580,7 @@
|
|
|
397
580
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
398
581
|
<div
|
|
399
582
|
onclick={() => openCreateModal(d)}
|
|
400
|
-
class="col-span-1 divide-y divide-[var(--ui-border)]/50 relative hover:bg-[var(--ui-secondary)]/10 cursor-pointer"
|
|
583
|
+
class="col-span-1 divide-y divide-[var(--ui-border)]/50 relative hover:bg-[var(--ui-secondary)]/10 cursor-pointer select-none"
|
|
401
584
|
>
|
|
402
585
|
{#each hours as hour}
|
|
403
586
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -423,12 +606,22 @@
|
|
|
423
606
|
</div>
|
|
424
607
|
{/if}
|
|
425
608
|
|
|
426
|
-
<!-- Render Event Boxes -->
|
|
427
|
-
{#each
|
|
428
|
-
{@const
|
|
429
|
-
{@const
|
|
430
|
-
|
|
431
|
-
|
|
609
|
+
<!-- Render Event Boxes with Collision Overlap Calculation & Resizing -->
|
|
610
|
+
{#each layoutEvents as ev (ev.id)}
|
|
611
|
+
{@const evStartTime = getTimeInZone(new Date(ev.start))}
|
|
612
|
+
{@const evEndTime = getTimeInZone(new Date(ev.end))}
|
|
613
|
+
{@const startMinutes = evStartTime.hours * 60 + evStartTime.minutes}
|
|
614
|
+
{@const endMinutes = evEndTime.hours * 60 + evEndTime.minutes}
|
|
615
|
+
{@const durationMinutes = Math.max(15, endMinutes > startMinutes ? endMinutes - startMinutes : 60)}
|
|
616
|
+
{@const topPos = (startMinutes / 60) * 56}
|
|
617
|
+
{@const heightPos = Math.max(26, (durationMinutes / 60) * 56)}
|
|
618
|
+
{@const widthPercent = 100 / ev.colTotal}
|
|
619
|
+
{@const leftPercent = ev.colIndex * widthPercent}
|
|
620
|
+
|
|
621
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
622
|
+
<div
|
|
623
|
+
role="button"
|
|
624
|
+
tabindex="0"
|
|
432
625
|
draggable="true"
|
|
433
626
|
ondragstart={(e) => {
|
|
434
627
|
draggedEventId = ev.id;
|
|
@@ -438,12 +631,25 @@
|
|
|
438
631
|
e.stopPropagation();
|
|
439
632
|
onEventClick?.(ev);
|
|
440
633
|
}}
|
|
441
|
-
|
|
442
|
-
|
|
634
|
+
onkeydown={(e) => {
|
|
635
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
636
|
+
e.preventDefault();
|
|
637
|
+
onEventClick?.(ev);
|
|
638
|
+
}
|
|
639
|
+
}}
|
|
640
|
+
class="group/event absolute rounded p-1 text-left text-[11px] text-white shadow-xs transition-opacity hover:opacity-95 overflow-hidden z-10 cursor-grab active:cursor-grabbing {draggedEventId === ev.id ? 'opacity-40 ring-2 ring-white' : ''}"
|
|
641
|
+
style="top: {topPos}px; height: {heightPos}px; left: calc({leftPercent}% + 2px); width: calc({widthPercent}% - 4px); background-color: {ev.color || 'var(--ui-primary)'};"
|
|
443
642
|
>
|
|
444
|
-
<div class="font-bold truncate">{ev.title}</div>
|
|
445
|
-
<div class="text-[9px] opacity-
|
|
446
|
-
|
|
643
|
+
<div class="font-bold truncate leading-tight">{ev.title}</div>
|
|
644
|
+
<div class="text-[9px] opacity-85 leading-tight">{formatTime(ev.start)} - {formatTime(ev.end)}</div>
|
|
645
|
+
|
|
646
|
+
<!-- Bottom edge resize handle (FullCalendar 1:1) -->
|
|
647
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
648
|
+
<div
|
|
649
|
+
class="absolute inset-x-0 bottom-0 h-2 cursor-s-resize opacity-0 group-hover/event:opacity-100 bg-white/20 hover:bg-white/40 transition-opacity z-20"
|
|
650
|
+
onmousedown={(e) => handleResizeStart(e, ev)}
|
|
651
|
+
></div>
|
|
652
|
+
</div>
|
|
447
653
|
{/each}
|
|
448
654
|
</div>
|
|
449
655
|
{/each}
|
|
@@ -471,7 +677,7 @@
|
|
|
471
677
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
472
678
|
<div
|
|
473
679
|
onclick={() => openCreateModal(currentDate)}
|
|
474
|
-
class="col-span-10 divide-y divide-[var(--ui-border)]/50 relative p-1 hover:bg-[var(--ui-secondary)]/10 cursor-pointer"
|
|
680
|
+
class="col-span-10 divide-y divide-[var(--ui-border)]/50 relative p-1 hover:bg-[var(--ui-secondary)]/10 cursor-pointer select-none"
|
|
475
681
|
>
|
|
476
682
|
{#each hours as _}
|
|
477
683
|
<div class="h-16"></div>
|
|
@@ -490,21 +696,49 @@
|
|
|
490
696
|
</div>
|
|
491
697
|
{/if}
|
|
492
698
|
|
|
493
|
-
{#each
|
|
494
|
-
{@const
|
|
495
|
-
{@const
|
|
496
|
-
|
|
497
|
-
|
|
699
|
+
{#each computeOverlappingLayout(getTimedEventsForDay(currentDate)) as ev (ev.id)}
|
|
700
|
+
{@const evStartTime = getTimeInZone(new Date(ev.start))}
|
|
701
|
+
{@const evEndTime = getTimeInZone(new Date(ev.end))}
|
|
702
|
+
{@const startMinutes = evStartTime.hours * 60 + evStartTime.minutes}
|
|
703
|
+
{@const endMinutes = evEndTime.hours * 60 + evEndTime.minutes}
|
|
704
|
+
{@const durationMinutes = Math.max(15, endMinutes > startMinutes ? endMinutes - startMinutes : 60)}
|
|
705
|
+
{@const topPos = (startMinutes / 60) * 64}
|
|
706
|
+
{@const heightPos = Math.max(32, (durationMinutes / 60) * 64)}
|
|
707
|
+
{@const widthPercent = 100 / ev.colTotal}
|
|
708
|
+
{@const leftPercent = ev.colIndex * widthPercent}
|
|
709
|
+
|
|
710
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
711
|
+
<div
|
|
712
|
+
role="button"
|
|
713
|
+
tabindex="0"
|
|
714
|
+
draggable="true"
|
|
715
|
+
ondragstart={(e) => {
|
|
716
|
+
draggedEventId = ev.id;
|
|
717
|
+
e.dataTransfer?.setData('text/plain', ev.id);
|
|
718
|
+
}}
|
|
498
719
|
onclick={(e) => {
|
|
499
720
|
e.stopPropagation();
|
|
500
721
|
onEventClick?.(ev);
|
|
501
722
|
}}
|
|
502
|
-
|
|
503
|
-
|
|
723
|
+
onkeydown={(e) => {
|
|
724
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
onEventClick?.(ev);
|
|
727
|
+
}
|
|
728
|
+
}}
|
|
729
|
+
class="group/event absolute rounded-lg p-2 text-left text-white shadow-sm transition-opacity hover:opacity-95 overflow-hidden z-10 cursor-grab active:cursor-grabbing {draggedEventId === ev.id ? 'opacity-40 ring-2 ring-white' : ''}"
|
|
730
|
+
style="top: {topPos}px; height: {heightPos}px; left: calc({leftPercent}% + 4px); width: calc({widthPercent}% - 8px); background-color: {ev.color || 'var(--ui-primary)'};"
|
|
504
731
|
>
|
|
505
|
-
<div class="font-bold text-xs">{ev.title}</div>
|
|
506
|
-
<div class="text-[10px] opacity-
|
|
507
|
-
|
|
732
|
+
<div class="font-bold text-xs truncate leading-tight">{ev.title}</div>
|
|
733
|
+
<div class="text-[10px] opacity-85 leading-tight">{formatTime(ev.start)} - {formatTime(ev.end)} {#if ev.description}• {ev.description}{/if}</div>
|
|
734
|
+
|
|
735
|
+
<!-- Bottom resize handle -->
|
|
736
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
737
|
+
<div
|
|
738
|
+
class="absolute inset-x-0 bottom-0 h-2.5 cursor-s-resize opacity-0 group-hover/event:opacity-100 bg-white/20 hover:bg-white/40 transition-opacity z-20"
|
|
739
|
+
onmousedown={(e) => handleResizeStart(e, ev)}
|
|
740
|
+
></div>
|
|
741
|
+
</div>
|
|
508
742
|
{/each}
|
|
509
743
|
</div>
|
|
510
744
|
</div>
|
|
@@ -25,6 +25,11 @@ type $$ComponentProps = {
|
|
|
25
25
|
newStart: Date;
|
|
26
26
|
newEnd: Date;
|
|
27
27
|
}) => void;
|
|
28
|
+
onEventResize?: (detail: {
|
|
29
|
+
event: CalendarEvent;
|
|
30
|
+
newStart: Date;
|
|
31
|
+
newEnd: Date;
|
|
32
|
+
}) => void;
|
|
28
33
|
};
|
|
29
34
|
declare const EventCalendar: import("svelte").Component<$$ComponentProps, {}, "events" | "view" | "currentDate">;
|
|
30
35
|
type EventCalendar = ReturnType<typeof EventCalendar>;
|