@intinyagroup/calendar 0.1.1-alpha.11 → 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/DatePicker.svelte
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import * as Popover from '@intinyagroup/ui';
|
|
3
3
|
import { Button, Input } from '@intinyagroup/ui';
|
|
4
|
-
import
|
|
4
|
+
import Calendar, { type CalendarDate } from './Calendar.svelte';
|
|
5
5
|
import { Calendar as CalendarIcon, X } from 'lucide-svelte';
|
|
6
|
-
import { cn } from '@intinyagroup/
|
|
6
|
+
import { cn } from '@intinyagroup/ui/utils';
|
|
7
7
|
|
|
8
8
|
let {
|
|
9
9
|
value = $bindable(null),
|
|
@@ -186,14 +186,23 @@
|
|
|
186
186
|
</script>
|
|
187
187
|
|
|
188
188
|
<div class={cn('relative inline-block w-full max-w-sm', className)}>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
190
|
+
<div
|
|
191
|
+
role="button"
|
|
192
|
+
tabindex={disabled ? -1 : 0}
|
|
193
|
+
aria-disabled={disabled}
|
|
194
|
+
onclick={() => { if (!disabled) isOpen = !isOpen; }}
|
|
195
|
+
onkeydown={(e) => {
|
|
196
|
+
if (disabled) return;
|
|
197
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
isOpen = !isOpen;
|
|
200
|
+
}
|
|
201
|
+
}}
|
|
193
202
|
class={cn(
|
|
194
|
-
'flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 text-sm transition-colors text-left',
|
|
203
|
+
'flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 text-sm transition-colors text-left cursor-pointer select-none',
|
|
195
204
|
'hover:border-[var(--ui-border)] focus:outline-none focus:ring-2 focus:ring-[var(--ui-ring)]/20 focus:border-[var(--ui-primary)]',
|
|
196
|
-
disabled && 'opacity-50 cursor-not-allowed',
|
|
205
|
+
disabled && 'opacity-50 cursor-not-allowed pointer-events-none',
|
|
197
206
|
value.start ? 'text-[var(--ui-foreground)]' : 'text-[var(--ui-muted-foreground)]'
|
|
198
207
|
)}
|
|
199
208
|
>
|
|
@@ -205,14 +214,17 @@
|
|
|
205
214
|
{#if value.start && !disabled}
|
|
206
215
|
<button
|
|
207
216
|
type="button"
|
|
208
|
-
onclick={
|
|
217
|
+
onclick={(e) => {
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
clearRange();
|
|
220
|
+
}}
|
|
209
221
|
class="p-1 rounded-full hover:bg-[var(--ui-secondary)] text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)]"
|
|
210
222
|
aria-label="Clear date range"
|
|
211
223
|
>
|
|
212
224
|
<X class="size-3.5" />
|
|
213
225
|
</button>
|
|
214
226
|
{/if}
|
|
215
|
-
</
|
|
227
|
+
</div>
|
|
216
228
|
|
|
217
229
|
{#if isOpen}
|
|
218
230
|
<!-- Backdrop for click outside -->
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
onEventClick,
|
|
45
45
|
onDateClick,
|
|
46
46
|
onAddEvent,
|
|
47
|
+
onEventReschedule,
|
|
47
48
|
}: {
|
|
48
49
|
events?: CalendarEvent[];
|
|
49
50
|
view?: CalendarView;
|
|
@@ -56,17 +57,152 @@
|
|
|
56
57
|
onEventClick?: (event: CalendarEvent) => void;
|
|
57
58
|
onDateClick?: (date: Date) => void;
|
|
58
59
|
onAddEvent?: (newEvent: CalendarEvent) => void;
|
|
60
|
+
onEventReschedule?: (detail: { event: CalendarEvent; newStart: Date; newEnd: Date }) => void;
|
|
61
|
+
onEventResize?: (detail: { event: CalendarEvent; newStart: Date; newEnd: Date }) => void;
|
|
59
62
|
} = $props();
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleEventDrop(targetDate: Date, targetHour?: number) {
|
|
77
|
+
if (!draggedEventId) return;
|
|
78
|
+
const ev = events.find((e) => e.id === draggedEventId);
|
|
79
|
+
if (!ev) return;
|
|
80
|
+
|
|
81
|
+
const origDurationMs = new Date(ev.end).getTime() - new Date(ev.start).getTime();
|
|
82
|
+
const newStart = new Date(targetDate);
|
|
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
|
+
}
|
|
88
|
+
const newEnd = new Date(newStart.getTime() + origDurationMs);
|
|
89
|
+
|
|
90
|
+
events = events.map((e) =>
|
|
91
|
+
e.id === draggedEventId ? { ...e, start: newStart, end: newEnd } : e
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
onEventReschedule?.({ event: ev, newStart, newEnd });
|
|
95
|
+
draggedEventId = null;
|
|
96
|
+
}
|
|
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
|
+
}
|
|
63
197
|
|
|
64
198
|
const activeTimeZone = $derived(
|
|
65
199
|
timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
66
200
|
);
|
|
67
201
|
|
|
68
|
-
const
|
|
202
|
+
const activeDateFnsLocale = $derived(locale.startsWith('id') ? localeId : localeEn);
|
|
203
|
+
const weekStartsOn = $derived(firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6);
|
|
69
204
|
|
|
205
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
70
206
|
// Day names localized and rotated according to firstDayOfWeek via date-fns
|
|
71
207
|
const dayNames = $derived.by(() => {
|
|
72
208
|
const start = startOfWeek(currentDate, { weekStartsOn });
|
|
@@ -116,7 +252,26 @@
|
|
|
116
252
|
});
|
|
117
253
|
|
|
118
254
|
function getEventsForDay(d: Date): CalendarEvent[] {
|
|
119
|
-
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
|
+
});
|
|
120
275
|
}
|
|
121
276
|
|
|
122
277
|
function getTimeInZone(date: Date): { hours: number; minutes: number } {
|
|
@@ -293,12 +448,18 @@
|
|
|
293
448
|
onDateClick?.(date);
|
|
294
449
|
openCreateModal(date);
|
|
295
450
|
}}
|
|
451
|
+
ondragover={(e) => e.preventDefault()}
|
|
452
|
+
ondrop={(e) => {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
e.stopPropagation();
|
|
455
|
+
handleEventDrop(date);
|
|
456
|
+
}}
|
|
296
457
|
class={cn(
|
|
297
|
-
'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',
|
|
298
459
|
!currentMonth && 'bg-[var(--ui-muted)]/15 opacity-50'
|
|
299
460
|
)}
|
|
300
461
|
>
|
|
301
|
-
<div class="flex items-center justify-between mb-1">
|
|
462
|
+
<div class="flex items-center justify-between mb-0.5 px-1">
|
|
302
463
|
<span
|
|
303
464
|
class={cn(
|
|
304
465
|
'text-xs font-semibold size-6 flex items-center justify-center rounded-full',
|
|
@@ -314,27 +475,44 @@
|
|
|
314
475
|
{/if}
|
|
315
476
|
</div>
|
|
316
477
|
|
|
317
|
-
<!-- Event pills -->
|
|
478
|
+
<!-- Event pills with spanning indicator -->
|
|
318
479
|
<div class="flex flex-col gap-1 overflow-hidden">
|
|
319
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
|
+
|
|
320
485
|
<button
|
|
321
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
|
+
}}
|
|
322
493
|
onclick={(e) => {
|
|
323
494
|
e.stopPropagation();
|
|
324
495
|
onEventClick?.(ev);
|
|
325
496
|
}}
|
|
326
497
|
class={cn(
|
|
327
|
-
'truncate
|
|
328
|
-
|
|
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' : ''
|
|
329
503
|
)}
|
|
330
|
-
style=
|
|
504
|
+
style="background-color: {ev.color || 'var(--ui-primary)'};"
|
|
331
505
|
>
|
|
332
|
-
{
|
|
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}
|
|
333
511
|
</button>
|
|
334
512
|
{/each}
|
|
335
513
|
|
|
336
514
|
{#if dayEvents.length > 3}
|
|
337
|
-
<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">
|
|
338
516
|
+{dayEvents.length - 3} more
|
|
339
517
|
</span>
|
|
340
518
|
{/if}
|
|
@@ -356,6 +534,31 @@
|
|
|
356
534
|
{/each}
|
|
357
535
|
</div>
|
|
358
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
|
+
|
|
359
562
|
<div class="grid grid-cols-8 divide-x divide-[var(--ui-border)] max-h-[600px] overflow-y-auto">
|
|
360
563
|
<!-- Hours Column -->
|
|
361
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)]">
|
|
@@ -366,7 +569,8 @@
|
|
|
366
569
|
|
|
367
570
|
<!-- 7 Days Grid Columns -->
|
|
368
571
|
{#each weekDays as d}
|
|
369
|
-
{@const dayEvents =
|
|
572
|
+
{@const dayEvents = getTimedEventsForDay(d)}
|
|
573
|
+
{@const layoutEvents = computeOverlappingLayout(dayEvents)}
|
|
370
574
|
{@const isCurrentDay = isToday(d)}
|
|
371
575
|
{@const nowTime = getTimeInZone(new Date())}
|
|
372
576
|
{@const nowMinutes = nowTime.hours * 60 + nowTime.minutes}
|
|
@@ -376,10 +580,19 @@
|
|
|
376
580
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
377
581
|
<div
|
|
378
582
|
onclick={() => openCreateModal(d)}
|
|
379
|
-
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"
|
|
380
584
|
>
|
|
381
|
-
{#each hours as
|
|
382
|
-
|
|
585
|
+
{#each hours as hour}
|
|
586
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
587
|
+
<div
|
|
588
|
+
class="h-14 transition-colors hover:bg-[var(--ui-primary)]/10"
|
|
589
|
+
ondragover={(e) => e.preventDefault()}
|
|
590
|
+
ondrop={(e) => {
|
|
591
|
+
e.preventDefault();
|
|
592
|
+
e.stopPropagation();
|
|
593
|
+
handleEventDrop(d, hour);
|
|
594
|
+
}}
|
|
595
|
+
></div>
|
|
383
596
|
{/each}
|
|
384
597
|
|
|
385
598
|
<!-- Google Calendar style Current Time Red Line -->
|
|
@@ -393,22 +606,50 @@
|
|
|
393
606
|
</div>
|
|
394
607
|
{/if}
|
|
395
608
|
|
|
396
|
-
<!-- Render Event Boxes -->
|
|
397
|
-
{#each
|
|
398
|
-
{@const
|
|
399
|
-
{@const
|
|
400
|
-
|
|
401
|
-
|
|
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"
|
|
625
|
+
draggable="true"
|
|
626
|
+
ondragstart={(e) => {
|
|
627
|
+
draggedEventId = ev.id;
|
|
628
|
+
e.dataTransfer?.setData('text/plain', ev.id);
|
|
629
|
+
}}
|
|
402
630
|
onclick={(e) => {
|
|
403
631
|
e.stopPropagation();
|
|
404
632
|
onEventClick?.(ev);
|
|
405
633
|
}}
|
|
406
|
-
|
|
407
|
-
|
|
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)'};"
|
|
408
642
|
>
|
|
409
|
-
<div class="font-bold truncate">{ev.title}</div>
|
|
410
|
-
<div class="text-[9px] opacity-
|
|
411
|
-
|
|
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>
|
|
412
653
|
{/each}
|
|
413
654
|
</div>
|
|
414
655
|
{/each}
|
|
@@ -432,23 +673,20 @@
|
|
|
432
673
|
{/each}
|
|
433
674
|
</div>
|
|
434
675
|
|
|
435
|
-
{@const isCurrentDay = isToday(currentDate)}
|
|
436
|
-
{@const nowTime = getTimeInZone(new Date())}
|
|
437
|
-
{@const nowMinutes = nowTime.hours * 60 + nowTime.minutes}
|
|
438
|
-
{@const currentTimeTop = (nowMinutes / 60) * 64}
|
|
439
|
-
|
|
440
676
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
441
677
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
442
678
|
<div
|
|
443
679
|
onclick={() => openCreateModal(currentDate)}
|
|
444
|
-
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"
|
|
445
681
|
>
|
|
446
682
|
{#each hours as _}
|
|
447
683
|
<div class="h-16"></div>
|
|
448
684
|
{/each}
|
|
449
685
|
|
|
450
686
|
<!-- Google Calendar style Current Time Red Line -->
|
|
451
|
-
{#if
|
|
687
|
+
{#if isToday(currentDate)}
|
|
688
|
+
{@const nowTime = getTimeInZone(new Date())}
|
|
689
|
+
{@const currentTimeTop = ((nowTime.hours * 60 + nowTime.minutes) / 60) * 64}
|
|
452
690
|
<div
|
|
453
691
|
class="absolute inset-x-0 z-30 pointer-events-none flex items-center"
|
|
454
692
|
style="top: {currentTimeTop}px;"
|
|
@@ -458,21 +696,49 @@
|
|
|
458
696
|
</div>
|
|
459
697
|
{/if}
|
|
460
698
|
|
|
461
|
-
{#each
|
|
462
|
-
{@const
|
|
463
|
-
{@const
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
}}
|
|
466
719
|
onclick={(e) => {
|
|
467
720
|
e.stopPropagation();
|
|
468
721
|
onEventClick?.(ev);
|
|
469
722
|
}}
|
|
470
|
-
|
|
471
|
-
|
|
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)'};"
|
|
472
731
|
>
|
|
473
|
-
<div class="font-bold text-xs">{ev.title}</div>
|
|
474
|
-
<div class="text-[10px] opacity-
|
|
475
|
-
|
|
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>
|
|
476
742
|
{/each}
|
|
477
743
|
</div>
|
|
478
744
|
</div>
|
|
@@ -500,28 +766,30 @@
|
|
|
500
766
|
|
|
501
767
|
<div class="space-y-3.5 text-xs">
|
|
502
768
|
<div>
|
|
503
|
-
<label class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">Title</label>
|
|
769
|
+
<label for="event-cal-title" class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">Title</label>
|
|
504
770
|
<input
|
|
771
|
+
id="event-cal-title"
|
|
505
772
|
type="text"
|
|
506
773
|
bind:value={newEventTitle}
|
|
507
774
|
placeholder="Event title (e.g. Sprint Review)..."
|
|
508
775
|
class="h-9 w-full rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 text-xs outline-none focus:border-[var(--ui-primary)]"
|
|
509
|
-
autofocus
|
|
510
776
|
/>
|
|
511
777
|
</div>
|
|
512
778
|
|
|
513
779
|
<div class="grid grid-cols-2 gap-3">
|
|
514
780
|
<div>
|
|
515
|
-
<label class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">Start Time</label>
|
|
781
|
+
<label for="event-cal-start-time" class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">Start Time</label>
|
|
516
782
|
<input
|
|
783
|
+
id="event-cal-start-time"
|
|
517
784
|
type="time"
|
|
518
785
|
bind:value={newEventStartTime}
|
|
519
786
|
class="h-9 w-full rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-2.5 text-xs outline-none focus:border-[var(--ui-primary)]"
|
|
520
787
|
/>
|
|
521
788
|
</div>
|
|
522
789
|
<div>
|
|
523
|
-
<label class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">End Time</label>
|
|
790
|
+
<label for="event-cal-end-time" class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">End Time</label>
|
|
524
791
|
<input
|
|
792
|
+
id="event-cal-end-time"
|
|
525
793
|
type="time"
|
|
526
794
|
bind:value={newEventEndTime}
|
|
527
795
|
class="h-9 w-full rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-2.5 text-xs outline-none focus:border-[var(--ui-primary)]"
|
|
@@ -530,7 +798,7 @@
|
|
|
530
798
|
</div>
|
|
531
799
|
|
|
532
800
|
<div>
|
|
533
|
-
<
|
|
801
|
+
<span class="block mb-1.5 font-semibold text-[var(--ui-muted-foreground)]">Tag Color</span>
|
|
534
802
|
<div class="flex items-center gap-2">
|
|
535
803
|
{#each presetColors as col}
|
|
536
804
|
<button
|
|
@@ -548,11 +816,10 @@
|
|
|
548
816
|
</div>
|
|
549
817
|
|
|
550
818
|
<div>
|
|
551
|
-
<label class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">Description (optional)</label>
|
|
819
|
+
<label for="event-cal-desc" class="block mb-1 font-semibold text-[var(--ui-muted-foreground)]">Description (optional)</label>
|
|
552
820
|
<textarea
|
|
821
|
+
id="event-cal-desc"
|
|
553
822
|
bind:value={newEventDescription}
|
|
554
|
-
rows={2}
|
|
555
|
-
placeholder="Notes, agenda, or link..."
|
|
556
823
|
class="w-full rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] p-2.5 text-xs outline-none focus:border-[var(--ui-primary)]"
|
|
557
824
|
></textarea>
|
|
558
825
|
</div>
|
|
@@ -20,6 +20,16 @@ type $$ComponentProps = {
|
|
|
20
20
|
onEventClick?: (event: CalendarEvent) => void;
|
|
21
21
|
onDateClick?: (date: Date) => void;
|
|
22
22
|
onAddEvent?: (newEvent: CalendarEvent) => void;
|
|
23
|
+
onEventReschedule?: (detail: {
|
|
24
|
+
event: CalendarEvent;
|
|
25
|
+
newStart: Date;
|
|
26
|
+
newEnd: Date;
|
|
27
|
+
}) => void;
|
|
28
|
+
onEventResize?: (detail: {
|
|
29
|
+
event: CalendarEvent;
|
|
30
|
+
newStart: Date;
|
|
31
|
+
newEnd: Date;
|
|
32
|
+
}) => void;
|
|
23
33
|
};
|
|
24
34
|
declare const EventCalendar: import("svelte").Component<$$ComponentProps, {}, "events" | "view" | "currentDate">;
|
|
25
35
|
type EventCalendar = ReturnType<typeof EventCalendar>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intinyagroup/calendar",
|
|
3
|
-
"version": "0.1.1-alpha.
|
|
3
|
+
"version": "0.1.1-alpha.13",
|
|
4
4
|
"description": "Calendar and DatePicker components for intinyagroup UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
"dev": "svelte-package -o dist -w"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@intinyagroup/ui": "0.1.1-alpha.
|
|
23
|
-
"date-fns": "^4.4.0"
|
|
22
|
+
"@intinyagroup/ui": "0.1.1-alpha.11",
|
|
23
|
+
"date-fns": "^4.4.0",
|
|
24
|
+
"lucide-svelte": "^1.0.1"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@sveltejs/package": "^2.5.8",
|