@softium/calendar 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,3275 @@
1
+ "use client";
2
+ import { Check, MoreVertical, Pencil, Trash2, ChevronDown, Plus, X, Minus, Repeat, ChevronLeft, ChevronRight, Undo2, Redo2, Search } from 'lucide-react';
3
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ // src/Calendar.tsx
7
+
8
+ // src/i18n.ts
9
+ function pick(language, ko, en) {
10
+ return language === "ko" ? ko : en;
11
+ }
12
+
13
+ // src/utils/color.ts
14
+ var COLOR_PRESETS = [
15
+ "#E30000",
16
+ "#FF9500",
17
+ "#FFCC00",
18
+ "#34C759",
19
+ "#30B0C7",
20
+ "#007AFF",
21
+ "#5856D6",
22
+ "#AF52DE",
23
+ "#FF3B30",
24
+ "#A2845E"
25
+ ];
26
+ var DEFAULT_COLOR = "#E30000";
27
+ function findClosestPresetColor(hex) {
28
+ const parse = (h) => {
29
+ const c = h.replace("#", "");
30
+ return [
31
+ Number.parseInt(c.substring(0, 2), 16),
32
+ Number.parseInt(c.substring(2, 4), 16),
33
+ Number.parseInt(c.substring(4, 6), 16)
34
+ ];
35
+ };
36
+ try {
37
+ const [r, g, b] = parse(hex);
38
+ let minDist = Number.POSITIVE_INFINITY;
39
+ let closest = COLOR_PRESETS[0];
40
+ for (const preset of COLOR_PRESETS) {
41
+ const [pr, pg, pb] = parse(preset);
42
+ const dist = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2;
43
+ if (dist < minDist) {
44
+ minDist = dist;
45
+ closest = preset;
46
+ }
47
+ }
48
+ return closest;
49
+ } catch {
50
+ return DEFAULT_COLOR;
51
+ }
52
+ }
53
+ function withAlpha(color, alpha) {
54
+ const hex = (color ?? DEFAULT_COLOR).replace("#", "");
55
+ const r = Number.parseInt(hex.substring(0, 2), 16);
56
+ const g = Number.parseInt(hex.substring(2, 4), 16);
57
+ const b = Number.parseInt(hex.substring(4, 6), 16);
58
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
59
+ }
60
+ function CategoryItem({
61
+ cat,
62
+ index,
63
+ checked,
64
+ language,
65
+ colorPalette,
66
+ editing,
67
+ onToggle,
68
+ onEditStart,
69
+ onEditCommit,
70
+ onEditCancel,
71
+ onDelete,
72
+ onDragStartRow,
73
+ onDragEnterRow,
74
+ onDropRow
75
+ }) {
76
+ const [menuOpen, setMenuOpen] = useState(false);
77
+ const [editName, setEditName] = useState(cat.name);
78
+ const [editColor, setEditColor] = useState(cat.color);
79
+ const [colorOpen, setColorOpen] = useState(false);
80
+ const rootRef = useRef(null);
81
+ useEffect(() => {
82
+ if (editing) {
83
+ setEditName(cat.name);
84
+ setEditColor(cat.color);
85
+ setColorOpen(false);
86
+ }
87
+ }, [editing, cat.name, cat.color]);
88
+ useEffect(() => {
89
+ if (!menuOpen) return;
90
+ const onDown = (e) => {
91
+ if (rootRef.current && !rootRef.current.contains(e.target)) setMenuOpen(false);
92
+ };
93
+ document.addEventListener("mousedown", onDown);
94
+ return () => document.removeEventListener("mousedown", onDown);
95
+ }, [menuOpen]);
96
+ if (editing) {
97
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow sft-cal-catrow--editing", ref: rootRef, children: [
98
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow__colorwrap", children: [
99
+ /* @__PURE__ */ jsx(
100
+ "button",
101
+ {
102
+ type: "button",
103
+ className: "sft-cal-catrow__swatch",
104
+ style: { background: editColor, borderColor: editColor },
105
+ onClick: () => setColorOpen((v) => !v),
106
+ "aria-label": "pick color"
107
+ }
108
+ ),
109
+ colorOpen && /* @__PURE__ */ jsx("div", { className: "sft-cal-colorgrid", children: colorPalette.map((c) => /* @__PURE__ */ jsx(
110
+ "button",
111
+ {
112
+ type: "button",
113
+ className: "sft-cal-colorgrid__swatch",
114
+ "data-active": editColor === c || void 0,
115
+ style: { background: c },
116
+ onClick: () => {
117
+ setEditColor(c);
118
+ setColorOpen(false);
119
+ },
120
+ "aria-label": c
121
+ },
122
+ c
123
+ )) })
124
+ ] }),
125
+ /* @__PURE__ */ jsx(
126
+ "input",
127
+ {
128
+ className: "sft-cal-catrow__name-input",
129
+ value: editName,
130
+ autoFocus: true,
131
+ onChange: (e) => setEditName(e.target.value),
132
+ onKeyDown: (e) => {
133
+ if (e.key === "Enter" && editName.trim())
134
+ onEditCommit(cat.id, { name: editName.trim(), color: editColor });
135
+ else if (e.key === "Escape") onEditCancel();
136
+ }
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ className: "sft-cal-btn sft-cal-btn--primary sft-cal-btn--sm",
144
+ disabled: !editName.trim(),
145
+ onClick: () => onEditCommit(cat.id, { name: editName.trim(), color: editColor }),
146
+ children: pick(language, "\uC800\uC7A5", "Save")
147
+ }
148
+ )
149
+ ] });
150
+ }
151
+ const onDragStart = (e) => {
152
+ e.dataTransfer.effectAllowed = "move";
153
+ onDragStartRow(index);
154
+ };
155
+ const onDragEnter = () => onDragEnterRow(index);
156
+ return /* @__PURE__ */ jsxs(
157
+ "div",
158
+ {
159
+ className: "sft-cal-catrow",
160
+ ref: rootRef,
161
+ draggable: true,
162
+ onDragStart,
163
+ onDragEnter,
164
+ onDragOver: (e) => e.preventDefault(),
165
+ onDrop: onDropRow,
166
+ onDragEnd: onDropRow,
167
+ children: [
168
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "sft-cal-catrow__main", onClick: () => onToggle(cat.id), children: [
169
+ /* @__PURE__ */ jsx(
170
+ "span",
171
+ {
172
+ className: "sft-cal-catrow__dot",
173
+ "data-checked": checked || void 0,
174
+ style: { background: cat.color, borderColor: cat.color },
175
+ children: checked && /* @__PURE__ */ jsx(Check, { size: 12, strokeWidth: 3 })
176
+ }
177
+ ),
178
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-catrow__name", children: cat.name })
179
+ ] }),
180
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow__menu", children: [
181
+ /* @__PURE__ */ jsx(
182
+ "button",
183
+ {
184
+ type: "button",
185
+ className: "sft-cal-catrow__menu-trigger",
186
+ "aria-label": "category menu",
187
+ onClick: (e) => {
188
+ e.stopPropagation();
189
+ setMenuOpen((v) => !v);
190
+ },
191
+ children: /* @__PURE__ */ jsx(MoreVertical, { size: 16 })
192
+ }
193
+ ),
194
+ menuOpen && /* @__PURE__ */ jsxs("div", { className: "sft-cal-menu", children: [
195
+ /* @__PURE__ */ jsxs(
196
+ "button",
197
+ {
198
+ type: "button",
199
+ className: "sft-cal-menu__item",
200
+ onClick: (e) => {
201
+ e.stopPropagation();
202
+ setMenuOpen(false);
203
+ onEditStart(cat);
204
+ },
205
+ children: [
206
+ /* @__PURE__ */ jsx(Pencil, { size: 14 }),
207
+ pick(language, "\uD3B8\uC9D1", "Edit")
208
+ ]
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsxs(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ className: "sft-cal-menu__item sft-cal-menu__item--danger",
216
+ onClick: (e) => {
217
+ e.stopPropagation();
218
+ setMenuOpen(false);
219
+ onDelete(cat.id);
220
+ },
221
+ children: [
222
+ /* @__PURE__ */ jsx(Trash2, { size: 14 }),
223
+ pick(language, "\uC0AD\uC81C", "Delete")
224
+ ]
225
+ }
226
+ )
227
+ ] })
228
+ ] })
229
+ ]
230
+ }
231
+ );
232
+ }
233
+ function CategoryFilter({
234
+ categories,
235
+ selectedCategoryIds,
236
+ language,
237
+ onToggle,
238
+ onAdd,
239
+ onUpdate,
240
+ onDelete,
241
+ onReorder
242
+ }) {
243
+ const [open, setOpen] = useState(false);
244
+ const [editingId, setEditingId] = useState(null);
245
+ const [showAdd, setShowAdd] = useState(false);
246
+ const [name, setName] = useState("");
247
+ const [color, setColor] = useState(COLOR_PRESETS[0]);
248
+ const [colorOpen, setColorOpen] = useState(false);
249
+ const rootRef = useRef(null);
250
+ const dragIndexRef = useRef(null);
251
+ useEffect(() => {
252
+ if (!open) return;
253
+ const onDown = (e) => {
254
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
255
+ };
256
+ document.addEventListener("mousedown", onDown);
257
+ return () => document.removeEventListener("mousedown", onDown);
258
+ }, [open]);
259
+ const allSelected = selectedCategoryIds.length === categories.length;
260
+ const label = allSelected ? pick(language, "\uC804\uCCB4 \uCE74\uD14C\uACE0\uB9AC", "All categories") : pick(
261
+ language,
262
+ `${selectedCategoryIds.length}\uAC1C \uC120\uD0DD\uB428`,
263
+ `${selectedCategoryIds.length} selected`
264
+ );
265
+ const usedColors = new Set(categories.map((c) => c.color));
266
+ const nextColor = COLOR_PRESETS.find((c) => !usedColors.has(c)) ?? COLOR_PRESETS[0];
267
+ const commitAdd = () => {
268
+ if (!name.trim()) return;
269
+ onAdd({ name: name.trim(), color });
270
+ setShowAdd(false);
271
+ setName("");
272
+ };
273
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-catfilter", ref: rootRef, children: [
274
+ /* @__PURE__ */ jsxs(
275
+ "button",
276
+ {
277
+ type: "button",
278
+ className: "sft-cal-catfilter__trigger",
279
+ "aria-haspopup": "true",
280
+ "aria-expanded": open,
281
+ onClick: () => setOpen((v) => !v),
282
+ children: [
283
+ /* @__PURE__ */ jsx("span", { children: label }),
284
+ /* @__PURE__ */ jsx(ChevronDown, { size: 16 })
285
+ ]
286
+ }
287
+ ),
288
+ open && /* @__PURE__ */ jsxs("div", { className: "sft-cal-catfilter__popover", children: [
289
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-catfilter__list", children: categories.map((cat, index) => /* @__PURE__ */ jsx(
290
+ CategoryItem,
291
+ {
292
+ cat,
293
+ index,
294
+ checked: selectedCategoryIds.includes(cat.id),
295
+ language,
296
+ colorPalette: COLOR_PRESETS,
297
+ editing: editingId === cat.id,
298
+ onToggle,
299
+ onEditStart: (c) => setEditingId(c.id),
300
+ onEditCommit: (id, data) => {
301
+ onUpdate(id, data);
302
+ setEditingId(null);
303
+ },
304
+ onEditCancel: () => setEditingId(null),
305
+ onDelete,
306
+ onDragStartRow: (i) => {
307
+ dragIndexRef.current = i;
308
+ },
309
+ onDragEnterRow: (i) => {
310
+ const from = dragIndexRef.current;
311
+ if (from != null && from !== i) {
312
+ onReorder(from, i);
313
+ dragIndexRef.current = i;
314
+ }
315
+ },
316
+ onDropRow: () => {
317
+ dragIndexRef.current = null;
318
+ }
319
+ },
320
+ cat.id
321
+ )) }),
322
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-modal__divider" }),
323
+ showAdd ? /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow sft-cal-catrow--editing", children: [
324
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow__colorwrap", children: [
325
+ /* @__PURE__ */ jsx(
326
+ "button",
327
+ {
328
+ type: "button",
329
+ className: "sft-cal-catrow__swatch",
330
+ style: { background: color, borderColor: color },
331
+ onClick: () => setColorOpen((v) => !v),
332
+ "aria-label": "pick color"
333
+ }
334
+ ),
335
+ colorOpen && /* @__PURE__ */ jsx("div", { className: "sft-cal-colorgrid", children: COLOR_PRESETS.map((c) => /* @__PURE__ */ jsx(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ className: "sft-cal-colorgrid__swatch",
340
+ "data-active": color === c || void 0,
341
+ style: { background: c },
342
+ onClick: () => {
343
+ setColor(c);
344
+ setColorOpen(false);
345
+ },
346
+ "aria-label": c
347
+ },
348
+ c
349
+ )) })
350
+ ] }),
351
+ /* @__PURE__ */ jsx(
352
+ "input",
353
+ {
354
+ className: "sft-cal-catrow__name-input",
355
+ autoFocus: true,
356
+ placeholder: pick(language, "\uCE74\uD14C\uACE0\uB9AC \uC774\uB984", "Category name"),
357
+ value: name,
358
+ onChange: (e) => setName(e.target.value),
359
+ onKeyDown: (e) => {
360
+ if (e.key === "Enter") commitAdd();
361
+ else if (e.key === "Escape") setShowAdd(false);
362
+ }
363
+ }
364
+ ),
365
+ /* @__PURE__ */ jsx(
366
+ "button",
367
+ {
368
+ type: "button",
369
+ className: "sft-cal-btn sft-cal-btn--primary sft-cal-btn--sm",
370
+ disabled: !name.trim(),
371
+ onClick: commitAdd,
372
+ children: pick(language, "\uC800\uC7A5", "Save")
373
+ }
374
+ )
375
+ ] }) : /* @__PURE__ */ jsxs(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ className: "sft-cal-modal__ghostrow",
380
+ onClick: () => {
381
+ setColor(nextColor);
382
+ setName("");
383
+ setShowAdd(true);
384
+ },
385
+ children: [
386
+ /* @__PURE__ */ jsx(Plus, { size: 14 }),
387
+ pick(language, "\uC0C8 \uCE74\uD14C\uACE0\uB9AC", "New category")
388
+ ]
389
+ }
390
+ )
391
+ ] })
392
+ ] });
393
+ }
394
+
395
+ // src/utils/date.ts
396
+ var monthNames = {
397
+ ko: ["1\uC6D4", "2\uC6D4", "3\uC6D4", "4\uC6D4", "5\uC6D4", "6\uC6D4", "7\uC6D4", "8\uC6D4", "9\uC6D4", "10\uC6D4", "11\uC6D4", "12\uC6D4"],
398
+ en: [
399
+ "January",
400
+ "February",
401
+ "March",
402
+ "April",
403
+ "May",
404
+ "June",
405
+ "July",
406
+ "August",
407
+ "September",
408
+ "October",
409
+ "November",
410
+ "December"
411
+ ]
412
+ };
413
+ var dayNames = {
414
+ ko: ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"],
415
+ en: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
416
+ };
417
+ function getDaysInMonth(date) {
418
+ const year = date.getFullYear();
419
+ const month = date.getMonth();
420
+ const firstDay = new Date(year, month, 1);
421
+ const lastDay = new Date(year, month + 1, 0);
422
+ const daysInMonth = lastDay.getDate();
423
+ const startingDayOfWeek = firstDay.getDay();
424
+ const days = [];
425
+ for (let i = 0; i < startingDayOfWeek; i++) {
426
+ days.push(new Date(year, month, -startingDayOfWeek + i + 1));
427
+ }
428
+ for (let i = 1; i <= daysInMonth; i++) {
429
+ days.push(new Date(year, month, i));
430
+ }
431
+ const weeksNeeded = Math.ceil(days.length / 7);
432
+ const remainingCells = weeksNeeded * 7 - days.length;
433
+ for (let i = 1; i <= remainingCells; i++) {
434
+ days.push(new Date(year, month + 1, i));
435
+ }
436
+ return { days, rows: weeksNeeded };
437
+ }
438
+ function getWeekDays(date) {
439
+ const day = date.getDay();
440
+ const sunday = new Date(date);
441
+ sunday.setDate(date.getDate() - day);
442
+ const days = [];
443
+ for (let i = 0; i < 7; i++) {
444
+ const weekDay = new Date(sunday);
445
+ weekDay.setDate(sunday.getDate() + i);
446
+ days.push(weekDay);
447
+ }
448
+ return days;
449
+ }
450
+ function isSameDay(a, b) {
451
+ if (!a || !b) return false;
452
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
453
+ }
454
+ function isToday(date) {
455
+ return isSameDay(date, /* @__PURE__ */ new Date());
456
+ }
457
+ function isCurrentMonth(date, currentDate) {
458
+ if (!date) return false;
459
+ return date.getMonth() === currentDate.getMonth();
460
+ }
461
+ function getPreviousPeriodDate(currentDate, viewType) {
462
+ const d = new Date(currentDate);
463
+ if (viewType === "day") d.setDate(currentDate.getDate() - 1);
464
+ else if (viewType === "week") d.setDate(currentDate.getDate() - 7);
465
+ else if (viewType === "year") return new Date(currentDate.getFullYear() - 1, 0, 1);
466
+ else return new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
467
+ return d;
468
+ }
469
+ function getNextPeriodDate(currentDate, viewType) {
470
+ const d = new Date(currentDate);
471
+ if (viewType === "day") d.setDate(currentDate.getDate() + 1);
472
+ else if (viewType === "week") d.setDate(currentDate.getDate() + 7);
473
+ else if (viewType === "year") return new Date(currentDate.getFullYear() + 1, 0, 1);
474
+ else return new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
475
+ return d;
476
+ }
477
+ function formatDate(date) {
478
+ const year = date.getFullYear();
479
+ const month = String(date.getMonth() + 1).padStart(2, "0");
480
+ const day = String(date.getDate()).padStart(2, "0");
481
+ return `${year}-${month}-${day}`;
482
+ }
483
+ function minutesFromY(clientY, rect) {
484
+ const ratio = (clientY - rect.top) / rect.height;
485
+ return Math.max(0, Math.min(24 * 60, ratio * 24 * 60));
486
+ }
487
+ function snapMinutes(minutes, step = 15) {
488
+ return Math.round(minutes / step) * step;
489
+ }
490
+ function minutesToHHMM(minutes) {
491
+ const m = Math.max(0, Math.min(23 * 60 + 59, Math.round(minutes)));
492
+ const h = Math.floor(m / 60);
493
+ const mm = m % 60;
494
+ return `${String(h).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
495
+ }
496
+ function hhmmToMinutes(hhmm) {
497
+ if (!hhmm) return 0;
498
+ const [h, m] = hhmm.split(":").map((n) => Number(n));
499
+ return (h || 0) * 60 + (m || 0);
500
+ }
501
+ var OPTIONS = [
502
+ { value: "this", ko: "\uC774 \uC77C\uC815\uB9CC", en: "This event only" },
503
+ { value: "following", ko: "\uC774\uD6C4 \uBAA8\uB4E0 \uC77C\uC815", en: "This and following events" },
504
+ { value: "all", ko: "\uBAA8\uB4E0 \uBC18\uBCF5 \uC77C\uC815", en: "All recurring events" }
505
+ ];
506
+ function DeleteOptionsDialog({
507
+ language,
508
+ selectedDeleteType,
509
+ setSelectedDeleteType,
510
+ onCancel,
511
+ onDelete
512
+ }) {
513
+ return /* @__PURE__ */ jsx("div", { className: "sft-cal-overlay", onMouseDown: onCancel, children: /* @__PURE__ */ jsxs(
514
+ "div",
515
+ {
516
+ className: "sft-cal-confirm",
517
+ role: "dialog",
518
+ "aria-modal": "true",
519
+ onMouseDown: (e) => e.stopPropagation(),
520
+ children: [
521
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-confirm__title", children: pick(language, "\uBC18\uBCF5 \uC77C\uC815 \uC0AD\uC81C", "Delete recurring event") }),
522
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-confirm__options", children: OPTIONS.map((o) => /* @__PURE__ */ jsxs(
523
+ "button",
524
+ {
525
+ type: "button",
526
+ className: "sft-cal-delopt",
527
+ "data-active": selectedDeleteType === o.value || void 0,
528
+ onClick: () => setSelectedDeleteType(o.value),
529
+ children: [
530
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-delopt__radio", "aria-hidden": "true" }),
531
+ pick(language, o.ko, o.en)
532
+ ]
533
+ },
534
+ o.value
535
+ )) }),
536
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-confirm__actions", children: [
537
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sft-cal-btn sft-cal-btn--outline", onClick: onCancel, children: pick(language, "\uCDE8\uC18C", "Cancel") }),
538
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sft-cal-btn sft-cal-btn--danger", onClick: onDelete, children: pick(language, "\uC0AD\uC81C", "Delete") })
539
+ ] })
540
+ ]
541
+ }
542
+ ) });
543
+ }
544
+ function SegmentTabs({
545
+ value,
546
+ onValueChange,
547
+ options,
548
+ block,
549
+ className
550
+ }) {
551
+ return /* @__PURE__ */ jsx(
552
+ "div",
553
+ {
554
+ className: `sft-cal-seg${block ? " sft-cal-seg--block" : ""}${className ? ` ${className}` : ""}`,
555
+ children: options.map((option) => /* @__PURE__ */ jsx(
556
+ "button",
557
+ {
558
+ type: "button",
559
+ className: "sft-cal-seg__btn",
560
+ "data-active": value === option.value || void 0,
561
+ onClick: () => onValueChange(option.value),
562
+ children: option.label
563
+ },
564
+ option.value
565
+ ))
566
+ }
567
+ );
568
+ }
569
+ var WEEKDAYS = [
570
+ { value: 0, ko: "\uC6D4", en: "M" },
571
+ { value: 1, ko: "\uD654", en: "T" },
572
+ { value: 2, ko: "\uC218", en: "W" },
573
+ { value: 3, ko: "\uBAA9", en: "T" },
574
+ { value: 4, ko: "\uAE08", en: "F" },
575
+ { value: 5, ko: "\uD1A0", en: "S" },
576
+ { value: 6, ko: "\uC77C", en: "S" }
577
+ ];
578
+ function RecurrenceSection({
579
+ language,
580
+ recurrenceFreq,
581
+ setRecurrenceFreq,
582
+ selectedWeekdays,
583
+ setSelectedWeekdays,
584
+ recurrenceEndType,
585
+ setRecurrenceEndType,
586
+ recurrenceCount,
587
+ setRecurrenceCount,
588
+ setRecurrenceEndDate,
589
+ internalStartDate
590
+ }) {
591
+ const toggleWeekday = (value) => {
592
+ if (selectedWeekdays.includes(value)) {
593
+ setSelectedWeekdays(selectedWeekdays.filter((d) => d !== value));
594
+ } else {
595
+ setSelectedWeekdays([...selectedWeekdays, value].sort((a, b) => a - b));
596
+ }
597
+ };
598
+ const onEndTypeChange = (type) => {
599
+ setRecurrenceEndType(type);
600
+ if (type === "date" && internalStartDate) {
601
+ const oneYearLater = new Date(internalStartDate);
602
+ oneYearLater.setFullYear(oneYearLater.getFullYear() + 1);
603
+ setRecurrenceEndDate(oneYearLater);
604
+ }
605
+ };
606
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-recur", children: [
607
+ /* @__PURE__ */ jsx(
608
+ SegmentTabs,
609
+ {
610
+ block: true,
611
+ value: recurrenceFreq,
612
+ onValueChange: (v) => setRecurrenceFreq(v),
613
+ options: [
614
+ { value: "DAILY", label: pick(language, "\uB9E4\uC77C", "Daily") },
615
+ { value: "WEEKLY", label: pick(language, "\uB9E4\uC8FC", "Weekly") },
616
+ { value: "MONTHLY", label: pick(language, "\uB9E4\uC6D4", "Monthly") },
617
+ { value: "YEARLY", label: pick(language, "\uB9E4\uB144", "Yearly") }
618
+ ]
619
+ }
620
+ ),
621
+ recurrenceFreq === "WEEKLY" && /* @__PURE__ */ jsxs("div", { className: "sft-cal-recur__block", children: [
622
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uBC18\uBCF5 \uC694\uC77C", "Repeat on") }),
623
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-recur__weekdays", children: WEEKDAYS.map((d) => /* @__PURE__ */ jsx(
624
+ "button",
625
+ {
626
+ type: "button",
627
+ className: "sft-cal-recur__weekday",
628
+ "data-active": selectedWeekdays.includes(d.value) || void 0,
629
+ onClick: () => toggleWeekday(d.value),
630
+ children: pick(language, d.ko, d.en)
631
+ },
632
+ d.value
633
+ )) })
634
+ ] }),
635
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-recur__block", children: [
636
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uC885\uB8CC", "Ends") }),
637
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-recur__end", children: [
638
+ /* @__PURE__ */ jsx(
639
+ SegmentTabs,
640
+ {
641
+ block: true,
642
+ value: recurrenceEndType,
643
+ onValueChange: (v) => onEndTypeChange(v),
644
+ options: [
645
+ { value: "never", label: pick(language, "\uC5C6\uC74C", "Never") },
646
+ { value: "date", label: pick(language, "\uB0A0\uC9DC", "Date") },
647
+ { value: "count", label: pick(language, "\uD69F\uC218", "Count") }
648
+ ]
649
+ }
650
+ ),
651
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-recur__count", children: recurrenceEndType === "count" && /* @__PURE__ */ jsxs(Fragment, { children: [
652
+ /* @__PURE__ */ jsx(
653
+ "input",
654
+ {
655
+ className: "sft-cal-input sft-cal-recur__count-input",
656
+ type: "text",
657
+ inputMode: "numeric",
658
+ value: String(recurrenceCount),
659
+ onChange: (e) => {
660
+ const v = e.target.value;
661
+ if (v === "") setRecurrenceCount("");
662
+ else if (/^[1-9]\d*$/.test(v)) setRecurrenceCount(Number.parseInt(v, 10));
663
+ },
664
+ onBlur: () => {
665
+ const n = Number(recurrenceCount);
666
+ if (recurrenceCount === "" || n < 1) setRecurrenceCount(1);
667
+ }
668
+ }
669
+ ),
670
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-recur__count-unit", children: pick(language, "\uD68C", "times") })
671
+ ] }) })
672
+ ] })
673
+ ] })
674
+ ] });
675
+ }
676
+ var toDateInput = (d) => d ? formatDate(d) : "";
677
+ var fromDateInput = (v) => {
678
+ if (!v) return void 0;
679
+ const parts = v.split("-").map(Number);
680
+ return new Date(parts[0] ?? 1970, (parts[1] ?? 1) - 1, parts[2] ?? 1);
681
+ };
682
+ var timeToMinutes = (t) => {
683
+ const parts = t.split(":");
684
+ const h = Number(parts[0] ?? 0);
685
+ const m = Number(parts[1] ?? 0);
686
+ return h * 60 + m;
687
+ };
688
+ var minutesToTime = (mins) => {
689
+ const clamped = Math.max(0, Math.min(23 * 60 + 59, mins));
690
+ const h = Math.floor(clamped / 60);
691
+ const m = clamped % 60;
692
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
693
+ };
694
+ var weekdayOf = (d) => (d.getDay() + 6) % 7;
695
+ function EventModal({
696
+ open,
697
+ onOpenChange,
698
+ event,
699
+ selectedDate,
700
+ selectedEndDate,
701
+ defaultStartTime = "09:00",
702
+ defaultEndTime = "10:00",
703
+ categories,
704
+ selectedCategoryIds,
705
+ language,
706
+ onSave,
707
+ onDelete,
708
+ onChange,
709
+ onToggleCategory,
710
+ onAddCategory,
711
+ onUpdateCategory,
712
+ onDeleteCategory,
713
+ onReorderCategories
714
+ }) {
715
+ const [title, setTitle] = useState("");
716
+ const [startTime, setStartTime] = useState(defaultStartTime);
717
+ const [endTime, setEndTime] = useState(defaultEndTime);
718
+ const [description, setDescription] = useState("");
719
+ const [categoryId, setCategoryId] = useState(categories[0]?.id ?? "");
720
+ const [startDate, setStartDate] = useState(void 0);
721
+ const [endDate, setEndDate] = useState(void 0);
722
+ const [titleFocused, setTitleFocused] = useState(false);
723
+ const [showDescription, setShowDescription] = useState(false);
724
+ const [isRecurring, setIsRecurring] = useState(false);
725
+ const [recurrenceFreq, setRecurrenceFreq] = useState("WEEKLY");
726
+ const [selectedWeekdays, setSelectedWeekdays] = useState([]);
727
+ const [recurrenceEndType, setRecurrenceEndType] = useState("never");
728
+ const [recurrenceEndDate, setRecurrenceEndDate] = useState(void 0);
729
+ const [recurrenceCount, setRecurrenceCount] = useState(10);
730
+ const [showAddCategory, setShowAddCategory] = useState(false);
731
+ const [newCategoryName, setNewCategoryName] = useState("");
732
+ const [newCategoryColor, setNewCategoryColor] = useState(COLOR_PRESETS[0]);
733
+ const [addColorOpen, setAddColorOpen] = useState(false);
734
+ const [editingCategoryId, setEditingCategoryId] = useState(null);
735
+ const [showDeleteOptions, setShowDeleteOptions] = useState(false);
736
+ const [deleteType, setDeleteType] = useState("this");
737
+ const dragIndexRef = useRef(null);
738
+ const prevStartRef = useRef(startTime);
739
+ const isEdit = !!event;
740
+ const isPeriod = useMemo(
741
+ () => !!startDate && !!endDate && !isSameDay(startDate, endDate),
742
+ [startDate, endDate]
743
+ );
744
+ useEffect(() => {
745
+ if (!open) return;
746
+ setShowDeleteOptions(false);
747
+ setShowAddCategory(false);
748
+ setEditingCategoryId(null);
749
+ if (event) {
750
+ setTitle(event.title ?? "");
751
+ setStartTime(event.startTime ?? defaultStartTime);
752
+ setEndTime(event.endTime ?? defaultEndTime);
753
+ setDescription(event.description ?? "");
754
+ setCategoryId(event.categoryId ?? categories[0]?.id ?? "");
755
+ setStartDate(event.date);
756
+ setEndDate(event.endDate ?? event.date);
757
+ setShowDescription(!!event.description);
758
+ const rec = event.recurrence;
759
+ setIsRecurring(!!rec);
760
+ if (rec) {
761
+ setRecurrenceFreq(rec.freq);
762
+ setSelectedWeekdays(rec.byweekday ?? []);
763
+ setRecurrenceEndType(rec.until ? "date" : rec.count ? "count" : "never");
764
+ setRecurrenceEndDate(rec.until);
765
+ setRecurrenceCount(rec.count ?? 10);
766
+ } else {
767
+ setSelectedWeekdays([]);
768
+ setRecurrenceEndType("never");
769
+ }
770
+ } else {
771
+ const s = selectedDate ?? /* @__PURE__ */ new Date();
772
+ setTitle("");
773
+ setStartTime(defaultStartTime);
774
+ setEndTime(defaultEndTime);
775
+ setDescription("");
776
+ setCategoryId(categories[0]?.id ?? "");
777
+ setStartDate(s);
778
+ setEndDate(selectedEndDate ?? s);
779
+ setShowDescription(false);
780
+ setIsRecurring(false);
781
+ setSelectedWeekdays([]);
782
+ setRecurrenceEndType("never");
783
+ setRecurrenceCount(10);
784
+ }
785
+ prevStartRef.current = event?.startTime ?? defaultStartTime;
786
+ }, [open]);
787
+ useEffect(() => {
788
+ if (!open) return;
789
+ const onKey = (e) => {
790
+ if (e.key === "Escape") onOpenChange(false);
791
+ };
792
+ window.addEventListener("keydown", onKey);
793
+ return () => window.removeEventListener("keydown", onKey);
794
+ }, [open, onOpenChange]);
795
+ const buildRecurrence = () => {
796
+ if (!isRecurring) return void 0;
797
+ const rec = { freq: recurrenceFreq, interval: 1 };
798
+ if (recurrenceFreq === "WEEKLY" && selectedWeekdays.length > 0)
799
+ rec.byweekday = selectedWeekdays;
800
+ if (recurrenceEndType === "date" && recurrenceEndDate) rec.until = recurrenceEndDate;
801
+ if (recurrenceEndType === "count") rec.count = Number(recurrenceCount) || 1;
802
+ return rec;
803
+ };
804
+ useEffect(() => {
805
+ if (!open || !onChange || !startDate) return;
806
+ onChange({
807
+ title,
808
+ startTime,
809
+ endTime,
810
+ description,
811
+ categoryId,
812
+ startDate,
813
+ endDate: isPeriod ? endDate : void 0,
814
+ recurrence: buildRecurrence()
815
+ });
816
+ }, [
817
+ open,
818
+ title,
819
+ startTime,
820
+ endTime,
821
+ description,
822
+ categoryId,
823
+ startDate,
824
+ endDate,
825
+ isPeriod,
826
+ isRecurring,
827
+ recurrenceFreq,
828
+ selectedWeekdays,
829
+ recurrenceEndType,
830
+ recurrenceEndDate,
831
+ recurrenceCount
832
+ ]);
833
+ const canSave = title.trim().length > 0 && !!startDate;
834
+ const handleSave = () => {
835
+ if (!canSave || !startDate) return;
836
+ onSave({
837
+ title: title.trim(),
838
+ startTime,
839
+ endTime,
840
+ description,
841
+ categoryId,
842
+ startDate,
843
+ endDate: isPeriod ? endDate : void 0,
844
+ recurrence: buildRecurrence()
845
+ });
846
+ onOpenChange(false);
847
+ };
848
+ const handleStartTimeChange = (v) => {
849
+ const duration = timeToMinutes(endTime) - timeToMinutes(prevStartRef.current);
850
+ setStartTime(v);
851
+ if (duration > 0) {
852
+ const nextEnd = minutesToTime(timeToMinutes(v) + duration);
853
+ if (timeToMinutes(nextEnd) >= timeToMinutes(v)) setEndTime(nextEnd);
854
+ }
855
+ prevStartRef.current = v;
856
+ };
857
+ const handleEndTimeChange = (v) => {
858
+ if (timeToMinutes(v) < timeToMinutes(startTime)) return;
859
+ setEndTime(v);
860
+ };
861
+ const toggleRecurring = (on) => {
862
+ setIsRecurring(on);
863
+ if (on && recurrenceFreq === "WEEKLY" && selectedWeekdays.length === 0 && startDate) {
864
+ setSelectedWeekdays([weekdayOf(startDate)]);
865
+ }
866
+ };
867
+ const handleDelete = () => {
868
+ if (event?.recurrence) setShowDeleteOptions(true);
869
+ else onDelete?.("all");
870
+ };
871
+ const usedColors = new Set(categories.map((c) => c.color));
872
+ const nextColor = COLOR_PRESETS.find((c) => !usedColors.has(c)) ?? COLOR_PRESETS[0];
873
+ if (!open) return null;
874
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-overlay", onMouseDown: (e) => e.stopPropagation(), children: [
875
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal", role: "dialog", "aria-modal": "true", children: [
876
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal__titlewrap", children: [
877
+ /* @__PURE__ */ jsx(
878
+ "input",
879
+ {
880
+ className: "sft-cal-modal__title",
881
+ value: title,
882
+ placeholder: pick(language, "\uC0C8 \uC77C\uC815", "New event"),
883
+ onFocus: () => setTitleFocused(true),
884
+ onBlur: () => setTitleFocused(false),
885
+ onChange: (e) => setTitle(e.target.value),
886
+ onKeyDown: (e) => {
887
+ if (e.key === "Enter" && !e.shiftKey) handleSave();
888
+ }
889
+ }
890
+ ),
891
+ /* @__PURE__ */ jsx(
892
+ "button",
893
+ {
894
+ type: "button",
895
+ className: "sft-cal-modal__close",
896
+ "aria-label": "close",
897
+ onClick: () => onOpenChange(false),
898
+ children: /* @__PURE__ */ jsx(X, { size: 18 })
899
+ }
900
+ ),
901
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-modal__underline", "data-active": titleFocused || void 0 })
902
+ ] }),
903
+ /* @__PURE__ */ jsxs("label", { className: "sft-cal-modal__repeat", children: [
904
+ /* @__PURE__ */ jsx("span", { children: pick(language, "\uBC18\uBCF5", "Repeat") }),
905
+ /* @__PURE__ */ jsx(
906
+ "input",
907
+ {
908
+ type: "checkbox",
909
+ className: "sft-cal-switch",
910
+ checked: isRecurring,
911
+ onChange: (e) => toggleRecurring(e.target.checked)
912
+ }
913
+ )
914
+ ] }),
915
+ isRecurring && /* @__PURE__ */ jsx(
916
+ RecurrenceSection,
917
+ {
918
+ language,
919
+ recurrenceFreq,
920
+ setRecurrenceFreq,
921
+ selectedWeekdays,
922
+ setSelectedWeekdays,
923
+ recurrenceEndType,
924
+ setRecurrenceEndType,
925
+ recurrenceCount,
926
+ setRecurrenceCount,
927
+ recurrenceEndDate,
928
+ setRecurrenceEndDate,
929
+ internalStartDate: startDate
930
+ }
931
+ ),
932
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal__grid2", children: [
933
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-field", children: [
934
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uC2DC\uC791 \uB0A0\uC9DC", "Start date") }),
935
+ /* @__PURE__ */ jsx(
936
+ "input",
937
+ {
938
+ type: "date",
939
+ className: "sft-cal-input",
940
+ value: toDateInput(startDate),
941
+ onChange: (e) => {
942
+ const d = fromDateInput(e.target.value);
943
+ setStartDate(d);
944
+ if (d && (!endDate || endDate < d)) setEndDate(d);
945
+ }
946
+ }
947
+ )
948
+ ] }),
949
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-field", children: [
950
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uC885\uB8CC \uB0A0\uC9DC", "End date") }),
951
+ isRecurring && recurrenceEndType !== "date" ? /* @__PURE__ */ jsx("div", { className: "sft-cal-input sft-cal-input--static", children: pick(language, "\uC885\uB8CC\uC77C \uC5C6\uC74C", "No end date") }) : isRecurring && recurrenceEndType === "date" ? /* @__PURE__ */ jsx(
952
+ "input",
953
+ {
954
+ type: "date",
955
+ className: "sft-cal-input",
956
+ value: toDateInput(recurrenceEndDate),
957
+ min: toDateInput(startDate),
958
+ onChange: (e) => setRecurrenceEndDate(fromDateInput(e.target.value))
959
+ }
960
+ ) : /* @__PURE__ */ jsx(
961
+ "input",
962
+ {
963
+ type: "date",
964
+ className: "sft-cal-input",
965
+ value: toDateInput(endDate),
966
+ min: toDateInput(startDate),
967
+ onChange: (e) => setEndDate(fromDateInput(e.target.value))
968
+ }
969
+ )
970
+ ] })
971
+ ] }),
972
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal__grid2", children: [
973
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-field", children: [
974
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uC2DC\uC791 \uC2DC\uAC04", "Start time") }),
975
+ /* @__PURE__ */ jsx(
976
+ "input",
977
+ {
978
+ type: "time",
979
+ className: "sft-cal-input",
980
+ value: startTime,
981
+ onChange: (e) => handleStartTimeChange(e.target.value)
982
+ }
983
+ )
984
+ ] }),
985
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-field", children: [
986
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uC885\uB8CC \uC2DC\uAC04", "End time") }),
987
+ /* @__PURE__ */ jsx(
988
+ "input",
989
+ {
990
+ type: "time",
991
+ className: "sft-cal-input",
992
+ value: endTime,
993
+ onChange: (e) => handleEndTimeChange(e.target.value)
994
+ }
995
+ )
996
+ ] })
997
+ ] }),
998
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal__category", children: [
999
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-modal__cathead", children: /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uCE74\uD14C\uACE0\uB9AC", "Category") }) }),
1000
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-modal__catlist", children: categories.map((cat, index) => /* @__PURE__ */ jsx(
1001
+ CategoryItem,
1002
+ {
1003
+ cat,
1004
+ index,
1005
+ checked: cat.id === categoryId,
1006
+ language,
1007
+ colorPalette: COLOR_PRESETS,
1008
+ editing: editingCategoryId === cat.id,
1009
+ onToggle: (id) => setCategoryId(id),
1010
+ onEditStart: (c) => setEditingCategoryId(c.id),
1011
+ onEditCommit: (id, data) => {
1012
+ onUpdateCategory(id, data);
1013
+ setEditingCategoryId(null);
1014
+ },
1015
+ onEditCancel: () => setEditingCategoryId(null),
1016
+ onDelete: (id) => {
1017
+ onDeleteCategory(id);
1018
+ if (categoryId === id)
1019
+ setCategoryId(categories.find((c) => c.id !== id)?.id ?? "");
1020
+ },
1021
+ onDragStartRow: (i) => {
1022
+ dragIndexRef.current = i;
1023
+ },
1024
+ onDragEnterRow: (i) => {
1025
+ const from = dragIndexRef.current;
1026
+ if (from != null && from !== i) {
1027
+ onReorderCategories(from, i);
1028
+ dragIndexRef.current = i;
1029
+ }
1030
+ },
1031
+ onDropRow: () => {
1032
+ dragIndexRef.current = null;
1033
+ }
1034
+ },
1035
+ cat.id
1036
+ )) }),
1037
+ showAddCategory ? /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow sft-cal-catrow--editing", children: [
1038
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-catrow__colorwrap", children: [
1039
+ /* @__PURE__ */ jsx(
1040
+ "button",
1041
+ {
1042
+ type: "button",
1043
+ className: "sft-cal-catrow__swatch",
1044
+ style: { background: newCategoryColor, borderColor: newCategoryColor },
1045
+ onClick: () => setAddColorOpen((v) => !v),
1046
+ "aria-label": "pick color"
1047
+ }
1048
+ ),
1049
+ addColorOpen && /* @__PURE__ */ jsx("div", { className: "sft-cal-colorgrid", children: COLOR_PRESETS.map((c) => /* @__PURE__ */ jsx(
1050
+ "button",
1051
+ {
1052
+ type: "button",
1053
+ className: "sft-cal-colorgrid__swatch",
1054
+ "data-active": newCategoryColor === c || void 0,
1055
+ style: { background: c },
1056
+ onClick: () => {
1057
+ setNewCategoryColor(c);
1058
+ setAddColorOpen(false);
1059
+ },
1060
+ "aria-label": c
1061
+ },
1062
+ c
1063
+ )) })
1064
+ ] }),
1065
+ /* @__PURE__ */ jsx(
1066
+ "input",
1067
+ {
1068
+ className: "sft-cal-catrow__name-input",
1069
+ autoFocus: true,
1070
+ placeholder: pick(language, "\uCE74\uD14C\uACE0\uB9AC \uC774\uB984", "Category name"),
1071
+ value: newCategoryName,
1072
+ onChange: (e) => setNewCategoryName(e.target.value),
1073
+ onKeyDown: (e) => {
1074
+ if (e.key === "Enter" && newCategoryName.trim()) {
1075
+ const id = onAddCategory({
1076
+ name: newCategoryName.trim(),
1077
+ color: newCategoryColor
1078
+ });
1079
+ setCategoryId(id);
1080
+ setShowAddCategory(false);
1081
+ setNewCategoryName("");
1082
+ } else if (e.key === "Escape") {
1083
+ setShowAddCategory(false);
1084
+ }
1085
+ }
1086
+ }
1087
+ ),
1088
+ /* @__PURE__ */ jsx(
1089
+ "button",
1090
+ {
1091
+ type: "button",
1092
+ className: "sft-cal-btn sft-cal-btn--primary sft-cal-btn--sm",
1093
+ disabled: !newCategoryName.trim(),
1094
+ onClick: () => {
1095
+ const id = onAddCategory({
1096
+ name: newCategoryName.trim(),
1097
+ color: newCategoryColor
1098
+ });
1099
+ setCategoryId(id);
1100
+ setShowAddCategory(false);
1101
+ setNewCategoryName("");
1102
+ },
1103
+ children: pick(language, "\uC800\uC7A5", "Save")
1104
+ }
1105
+ )
1106
+ ] }) : /* @__PURE__ */ jsxs(
1107
+ "button",
1108
+ {
1109
+ type: "button",
1110
+ className: "sft-cal-modal__ghostrow",
1111
+ onClick: () => {
1112
+ setNewCategoryColor(nextColor);
1113
+ setNewCategoryName("");
1114
+ setShowAddCategory(true);
1115
+ },
1116
+ children: [
1117
+ /* @__PURE__ */ jsx(Plus, { size: 14 }),
1118
+ pick(language, "\uC0C8 \uCE74\uD14C\uACE0\uB9AC", "New category")
1119
+ ]
1120
+ }
1121
+ )
1122
+ ] }),
1123
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-modal__divider" }),
1124
+ showDescription ? /* @__PURE__ */ jsxs("div", { className: "sft-cal-field", children: [
1125
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal__cathead", children: [
1126
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-label", children: pick(language, "\uC124\uBA85", "Description") }),
1127
+ /* @__PURE__ */ jsxs(
1128
+ "button",
1129
+ {
1130
+ type: "button",
1131
+ className: "sft-cal-modal__collapse",
1132
+ onClick: () => {
1133
+ setShowDescription(false);
1134
+ setDescription("");
1135
+ },
1136
+ children: [
1137
+ /* @__PURE__ */ jsx(Minus, { size: 14 }),
1138
+ pick(language, "\uC811\uAE30", "Collapse")
1139
+ ]
1140
+ }
1141
+ )
1142
+ ] }),
1143
+ /* @__PURE__ */ jsx(
1144
+ "textarea",
1145
+ {
1146
+ className: "sft-cal-textarea",
1147
+ rows: 2,
1148
+ autoFocus: true,
1149
+ value: description,
1150
+ onChange: (e) => setDescription(e.target.value)
1151
+ }
1152
+ )
1153
+ ] }) : /* @__PURE__ */ jsxs(
1154
+ "button",
1155
+ {
1156
+ type: "button",
1157
+ className: "sft-cal-modal__ghostrow",
1158
+ onClick: () => setShowDescription(true),
1159
+ children: [
1160
+ /* @__PURE__ */ jsx(Plus, { size: 14 }),
1161
+ pick(language, "\uC124\uBA85 \uCD94\uAC00", "Add description")
1162
+ ]
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-modal__actions", children: [
1166
+ /* @__PURE__ */ jsx(
1167
+ "button",
1168
+ {
1169
+ type: "button",
1170
+ className: "sft-cal-btn sft-cal-btn--outline sft-cal-btn--block",
1171
+ onClick: () => onOpenChange(false),
1172
+ children: pick(language, "\uCDE8\uC18C", "Cancel")
1173
+ }
1174
+ ),
1175
+ /* @__PURE__ */ jsx(
1176
+ "button",
1177
+ {
1178
+ type: "button",
1179
+ className: "sft-cal-btn sft-cal-btn--primary sft-cal-btn--block",
1180
+ disabled: !canSave,
1181
+ onClick: handleSave,
1182
+ children: isEdit ? pick(language, "\uC218\uC815", "Update") : pick(language, "\uCD94\uAC00", "Add")
1183
+ }
1184
+ ),
1185
+ isEdit && onDelete && /* @__PURE__ */ jsx(
1186
+ "button",
1187
+ {
1188
+ type: "button",
1189
+ className: "sft-cal-btn sft-cal-btn--outline sft-cal-btn--icon",
1190
+ "aria-label": pick(language, "\uC0AD\uC81C", "Delete"),
1191
+ onClick: handleDelete,
1192
+ children: /* @__PURE__ */ jsx(Trash2, { size: 16 })
1193
+ }
1194
+ )
1195
+ ] })
1196
+ ] }),
1197
+ showDeleteOptions && /* @__PURE__ */ jsx(
1198
+ DeleteOptionsDialog,
1199
+ {
1200
+ language,
1201
+ selectedDeleteType: deleteType,
1202
+ setSelectedDeleteType: setDeleteType,
1203
+ onCancel: () => setShowDeleteOptions(false),
1204
+ onDelete: () => {
1205
+ onDelete?.(deleteType);
1206
+ setShowDeleteOptions(false);
1207
+ onOpenChange(false);
1208
+ }
1209
+ }
1210
+ )
1211
+ ] });
1212
+ }
1213
+ var OPTIONS2 = [
1214
+ { value: "this", ko: "\uC774 \uC77C\uC815\uB9CC", en: "This event only" },
1215
+ { value: "following", ko: "\uC774\uD6C4 \uBAA8\uB4E0 \uC77C\uC815", en: "This and following events" },
1216
+ { value: "all", ko: "\uBAA8\uB4E0 \uBC18\uBCF5 \uC77C\uC815", en: "All recurring events" }
1217
+ ];
1218
+ function RecurrenceScopeDialog({
1219
+ language,
1220
+ selectedScope,
1221
+ setSelectedScope,
1222
+ onCancel,
1223
+ onConfirm
1224
+ }) {
1225
+ return /* @__PURE__ */ jsx("div", { className: "sft-cal-overlay", onMouseDown: onCancel, children: /* @__PURE__ */ jsxs(
1226
+ "div",
1227
+ {
1228
+ className: "sft-cal-confirm",
1229
+ role: "dialog",
1230
+ "aria-modal": "true",
1231
+ onMouseDown: (e) => e.stopPropagation(),
1232
+ children: [
1233
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-confirm__title", children: pick(language, "\uBC18\uBCF5 \uC77C\uC815 \uC774\uB3D9", "Move recurring event") }),
1234
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-confirm__options", children: OPTIONS2.map((o) => /* @__PURE__ */ jsxs(
1235
+ "button",
1236
+ {
1237
+ type: "button",
1238
+ className: "sft-cal-delopt sft-cal-delopt--accent",
1239
+ "data-active": selectedScope === o.value || void 0,
1240
+ onClick: () => setSelectedScope(o.value),
1241
+ children: [
1242
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-delopt__radio", "aria-hidden": "true" }),
1243
+ pick(language, o.ko, o.en)
1244
+ ]
1245
+ },
1246
+ o.value
1247
+ )) }),
1248
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-confirm__actions", children: [
1249
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sft-cal-btn sft-cal-btn--outline", onClick: onCancel, children: pick(language, "\uCDE8\uC18C", "Cancel") }),
1250
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sft-cal-btn sft-cal-btn--primary", onClick: onConfirm, children: pick(language, "\uC774\uB3D9", "Move") })
1251
+ ] })
1252
+ ]
1253
+ }
1254
+ ) });
1255
+ }
1256
+ var idSeq = 0;
1257
+ var uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${(idSeq++).toString(36)}`;
1258
+ var DEFAULT_CATEGORIES = [
1259
+ { id: "personal", name: "\uAC1C\uC778", color: "#E30000" },
1260
+ { id: "work", name: "\uC5C5\uBB34", color: "#007AFF" },
1261
+ { id: "holiday", name: "\uD734\uAC00", color: "#34C759" }
1262
+ ];
1263
+ function useCalendar(options = {}) {
1264
+ const [events, setEventsRaw] = useState(options.initialEvents ?? []);
1265
+ const [categories, setCategories] = useState(
1266
+ options.initialCategories ?? DEFAULT_CATEGORIES
1267
+ );
1268
+ const [selectedCategoryIds, setSelectedCategoryIds] = useState(
1269
+ () => (options.initialCategories ?? DEFAULT_CATEGORIES).map((c) => c.id)
1270
+ );
1271
+ const [past, setPast] = useState([]);
1272
+ const [future, setFuture] = useState([]);
1273
+ const eventsRef = useRef(events);
1274
+ eventsRef.current = events;
1275
+ const setEvents = useCallback((updater) => {
1276
+ setPast((p) => [...p, eventsRef.current]);
1277
+ setFuture([]);
1278
+ setEventsRaw(updater);
1279
+ }, []);
1280
+ const undo = useCallback(() => {
1281
+ setPast((p) => {
1282
+ if (!p.length) return p;
1283
+ const prev = p[p.length - 1];
1284
+ setFuture((f) => [...f, eventsRef.current]);
1285
+ setEventsRaw(prev);
1286
+ return p.slice(0, -1);
1287
+ });
1288
+ }, []);
1289
+ const redo = useCallback(() => {
1290
+ setFuture((f) => {
1291
+ if (!f.length) return f;
1292
+ const next = f[f.length - 1];
1293
+ setPast((p) => [...p, eventsRef.current]);
1294
+ setEventsRaw(next);
1295
+ return f.slice(0, -1);
1296
+ });
1297
+ }, []);
1298
+ const addEvent = useCallback(
1299
+ (data) => {
1300
+ const event = { ...data, id: uid("evt") };
1301
+ setEvents((prev) => [...prev, event]);
1302
+ return event;
1303
+ },
1304
+ [setEvents]
1305
+ );
1306
+ const updateEvent = useCallback(
1307
+ (id, data) => {
1308
+ setEvents((prev) => prev.map((e) => e.id === id ? { ...e, ...data } : e));
1309
+ },
1310
+ [setEvents]
1311
+ );
1312
+ const deleteEvent = useCallback(
1313
+ (id) => {
1314
+ setEvents((prev) => prev.filter((e) => e.id !== id));
1315
+ },
1316
+ [setEvents]
1317
+ );
1318
+ const addCategory = useCallback((data) => {
1319
+ const id = uid("cat");
1320
+ setCategories((prev) => [...prev, { id, name: data.name, color: data.color }]);
1321
+ setSelectedCategoryIds((prev) => [...prev, id]);
1322
+ return id;
1323
+ }, []);
1324
+ const updateCategory = useCallback((id, data) => {
1325
+ setCategories((prev) => prev.map((c) => c.id === id ? { ...c, ...data } : c));
1326
+ }, []);
1327
+ const deleteCategory = useCallback(
1328
+ (id) => {
1329
+ setCategories((prev) => prev.length <= 1 ? prev : prev.filter((c) => c.id !== id));
1330
+ setSelectedCategoryIds((prev) => prev.filter((c) => c !== id));
1331
+ setEvents((prev) => prev.filter((e) => e.categoryId !== id));
1332
+ },
1333
+ [setEvents]
1334
+ );
1335
+ const reorderCategories = useCallback((from, to) => {
1336
+ setCategories((prev) => {
1337
+ if (from === to || from < 0 || to < 0 || from >= prev.length || to >= prev.length)
1338
+ return prev;
1339
+ const next = [...prev];
1340
+ const [moved] = next.splice(from, 1);
1341
+ if (!moved) return prev;
1342
+ next.splice(to, 0, moved);
1343
+ return next;
1344
+ });
1345
+ }, []);
1346
+ const toggleCategory = useCallback((id) => {
1347
+ setSelectedCategoryIds(
1348
+ (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
1349
+ );
1350
+ }, []);
1351
+ return {
1352
+ events,
1353
+ setEvents,
1354
+ addEvent,
1355
+ updateEvent,
1356
+ deleteEvent,
1357
+ canUndo: past.length > 0,
1358
+ canRedo: future.length > 0,
1359
+ undo,
1360
+ redo,
1361
+ categories,
1362
+ setCategories,
1363
+ addCategory,
1364
+ updateCategory,
1365
+ deleteCategory,
1366
+ reorderCategories,
1367
+ selectedCategoryIds,
1368
+ toggleCategory,
1369
+ setSelectedCategoryIds
1370
+ };
1371
+ }
1372
+
1373
+ // src/utils/events.ts
1374
+ var toJsDay = (editorIndex) => (editorIndex + 1) % 7;
1375
+ var startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
1376
+ function occurrencesInRange(base, rec, from, to) {
1377
+ const interval = Math.max(1, rec.interval || 1);
1378
+ const out = [];
1379
+ const until = rec.until ? startOfDay(rec.until) : null;
1380
+ const maxCount = rec.count ?? Number.POSITIVE_INFINITY;
1381
+ const windowFrom = startOfDay(from);
1382
+ const windowTo = startOfDay(to);
1383
+ const GUARD = 5e3;
1384
+ let produced = 0;
1385
+ const consider = (d) => {
1386
+ const day = startOfDay(d);
1387
+ if (until && day > until) return false;
1388
+ if (produced >= maxCount) return false;
1389
+ produced += 1;
1390
+ if (day >= windowFrom && day <= windowTo) out.push(day);
1391
+ return true;
1392
+ };
1393
+ if (rec.freq === "WEEKLY" && rec.byweekday && rec.byweekday.length > 0) {
1394
+ const weekdays = [...rec.byweekday].map(toJsDay).sort((a, b) => a - b);
1395
+ const weekStart = startOfDay(base);
1396
+ weekStart.setDate(weekStart.getDate() - weekStart.getDay());
1397
+ for (let w = 0; w < GUARD; w++) {
1398
+ const wkStart = new Date(weekStart);
1399
+ wkStart.setDate(weekStart.getDate() + w * 7 * interval);
1400
+ if (wkStart > windowTo && produced > 0) break;
1401
+ let stop = false;
1402
+ for (const jsDay of weekdays) {
1403
+ const d = new Date(wkStart);
1404
+ d.setDate(wkStart.getDate() + jsDay);
1405
+ if (startOfDay(d) < startOfDay(base)) continue;
1406
+ if (!consider(d)) {
1407
+ stop = true;
1408
+ break;
1409
+ }
1410
+ }
1411
+ if (stop) break;
1412
+ if (produced >= maxCount) break;
1413
+ if (wkStart > windowTo) break;
1414
+ }
1415
+ return out;
1416
+ }
1417
+ for (let i = 0; i < GUARD; i++) {
1418
+ const d = new Date(base);
1419
+ if (rec.freq === "DAILY") d.setDate(base.getDate() + i * interval);
1420
+ else if (rec.freq === "WEEKLY") d.setDate(base.getDate() + i * 7 * interval);
1421
+ else if (rec.freq === "MONTHLY") d.setMonth(base.getMonth() + i * interval);
1422
+ else d.setFullYear(base.getFullYear() + i * interval);
1423
+ if (!consider(d)) break;
1424
+ if (startOfDay(d) > windowTo && produced > 0 && maxCount === Number.POSITIVE_INFINITY) break;
1425
+ }
1426
+ return out;
1427
+ }
1428
+ function expandRecurringEvents(events, startDate, endDate) {
1429
+ const expanded = [];
1430
+ for (const event of events) {
1431
+ if (!event.recurrence) {
1432
+ expanded.push(event);
1433
+ continue;
1434
+ }
1435
+ const occurrences = occurrencesInRange(event.date, event.recurrence, startDate, endDate);
1436
+ for (const occ of occurrences) {
1437
+ const excluded = event.exdate?.some((ex) => isSameDay(ex, occ));
1438
+ if (excluded) continue;
1439
+ const date = new Date(
1440
+ occ.getFullYear(),
1441
+ occ.getMonth(),
1442
+ occ.getDate(),
1443
+ event.date.getHours(),
1444
+ event.date.getMinutes()
1445
+ );
1446
+ expanded.push({
1447
+ ...event,
1448
+ id: `${event.id}-recur-${date.getTime()}`,
1449
+ date,
1450
+ isRecurringInstance: true,
1451
+ recurringEventId: event.id
1452
+ });
1453
+ }
1454
+ }
1455
+ return expanded;
1456
+ }
1457
+ function eventCoversDate(event, date) {
1458
+ if (isSameDay(event.date, date)) return true;
1459
+ if (!event.endDate) return false;
1460
+ const check = startOfDay(date).getTime();
1461
+ return check >= startOfDay(event.date).getTime() && check <= startOfDay(event.endDate).getTime();
1462
+ }
1463
+ function getEventsForDate(date, events, selectedCategoryIds) {
1464
+ if (!date) return [];
1465
+ return events.filter((event) => {
1466
+ if (!eventCoversDate(event, date)) return false;
1467
+ if (event.categoryId && !selectedCategoryIds.includes(event.categoryId)) return false;
1468
+ return true;
1469
+ }).sort((a, b) => {
1470
+ if (!a.startTime && !b.startTime) return 0;
1471
+ if (!a.startTime) return 1;
1472
+ if (!b.startTime) return -1;
1473
+ const ap = a.startTime.split(":");
1474
+ const bp = b.startTime.split(":");
1475
+ const aMin = Number(ap[0] ?? 0) * 60 + Number(ap[1] ?? 0);
1476
+ const bMin = Number(bp[0] ?? 0) * 60 + Number(bp[1] ?? 0);
1477
+ return aMin - bMin;
1478
+ });
1479
+ }
1480
+ function getNextAvailableTime(date, events) {
1481
+ if (!date) return "09:00";
1482
+ const dayEvents = events.filter((e) => isSameDay(e.date, date));
1483
+ for (let hour = 9; hour < 24; hour++) {
1484
+ const timeStr = `${hour.toString().padStart(2, "0")}:00`;
1485
+ if (!dayEvents.some((e) => e.startTime === timeStr)) return timeStr;
1486
+ }
1487
+ return "09:00";
1488
+ }
1489
+ var HOURS = Array.from({ length: 24 }, (_, i) => i);
1490
+ var MIN_EVENT_MINUTES = 15;
1491
+ function DayView({
1492
+ currentDate,
1493
+ events,
1494
+ selectedCategoryIds,
1495
+ categories,
1496
+ language,
1497
+ selectedEvent,
1498
+ previewEvent,
1499
+ onEventClick,
1500
+ onAddEventClick,
1501
+ onTimeRangeSelect,
1502
+ onEventTimeChange
1503
+ }) {
1504
+ const dayStart = new Date(currentDate);
1505
+ dayStart.setHours(0, 0, 0, 0);
1506
+ const dayEnd = new Date(currentDate);
1507
+ dayEnd.setHours(23, 59, 59, 999);
1508
+ const expanded = expandRecurringEvents(events, dayStart, dayEnd);
1509
+ const dayEvents = expanded.filter((event) => {
1510
+ const startMatch = event.date.getDate() === currentDate.getDate() && event.date.getMonth() === currentDate.getMonth() && event.date.getFullYear() === currentDate.getFullYear();
1511
+ let inRange = startMatch;
1512
+ if (!inRange && event.endDate) {
1513
+ const check = new Date(
1514
+ currentDate.getFullYear(),
1515
+ currentDate.getMonth(),
1516
+ currentDate.getDate()
1517
+ ).getTime();
1518
+ const s = new Date(
1519
+ event.date.getFullYear(),
1520
+ event.date.getMonth(),
1521
+ event.date.getDate()
1522
+ ).getTime();
1523
+ const e = new Date(
1524
+ event.endDate.getFullYear(),
1525
+ event.endDate.getMonth(),
1526
+ event.endDate.getDate()
1527
+ ).getTime();
1528
+ inRange = check >= s && check <= e;
1529
+ }
1530
+ return inRange;
1531
+ }).filter((event) => !event.categoryId || selectedCategoryIds.includes(event.categoryId));
1532
+ const dow = currentDate.getDay();
1533
+ const dayCls = dow === 0 ? "sft-cal-sun" : dow === 6 ? "sft-cal-sat" : void 0;
1534
+ const colRef = useRef(null);
1535
+ const createRef = useRef(null);
1536
+ const [createPreview, setCreatePreview] = useState(
1537
+ null
1538
+ );
1539
+ const moveRef = useRef(null);
1540
+ const justMovedRef = useRef(false);
1541
+ const [livePreview, setLivePreview] = useState(null);
1542
+ const createPreviewRef = useRef(null);
1543
+ const livePreviewRef = useRef(null);
1544
+ const [interacting, setInteracting] = useState(false);
1545
+ useEffect(() => {
1546
+ if (!interacting) return;
1547
+ function onMove(e) {
1548
+ const rect = colRef.current?.getBoundingClientRect();
1549
+ if (!rect || rect.height <= 0) return;
1550
+ if (createRef.current) {
1551
+ const m = snapMinutes(minutesFromY(e.clientY, rect));
1552
+ const next = { startMin: createRef.current.startMin, endMin: m };
1553
+ createPreviewRef.current = next;
1554
+ setCreatePreview(next);
1555
+ return;
1556
+ }
1557
+ const mv = moveRef.current;
1558
+ if (mv) {
1559
+ if (!mv.moved && Math.abs(e.clientY - mv.startY) > 4) mv.moved = true;
1560
+ if (!mv.moved) return;
1561
+ const deltaMin = snapMinutes((e.clientY - mv.startY) / rect.height * 24 * 60);
1562
+ let newStart = mv.origStart;
1563
+ let newEnd = mv.origEnd;
1564
+ if (mv.mode === "move") {
1565
+ newStart = mv.origStart + deltaMin;
1566
+ newEnd = mv.origEnd + deltaMin;
1567
+ if (newStart < 0) {
1568
+ newEnd -= newStart;
1569
+ newStart = 0;
1570
+ }
1571
+ if (newEnd > 24 * 60) {
1572
+ newStart -= newEnd - 24 * 60;
1573
+ newEnd = 24 * 60;
1574
+ }
1575
+ } else if (mv.mode === "resize-start") {
1576
+ newStart = Math.max(0, Math.min(mv.origEnd - MIN_EVENT_MINUTES, mv.origStart + deltaMin));
1577
+ } else {
1578
+ newEnd = Math.min(
1579
+ 24 * 60,
1580
+ Math.max(mv.origStart + MIN_EVENT_MINUTES, mv.origEnd + deltaMin)
1581
+ );
1582
+ }
1583
+ const next = { id: mv.event.id, startMin: newStart, endMin: newEnd };
1584
+ livePreviewRef.current = next;
1585
+ setLivePreview(next);
1586
+ }
1587
+ }
1588
+ function onUp() {
1589
+ if (createRef.current) {
1590
+ const p = createPreviewRef.current;
1591
+ if (p && Number.isFinite(p.startMin) && Number.isFinite(p.endMin)) {
1592
+ const s = Math.min(p.startMin, p.endMin);
1593
+ const e = Math.max(p.startMin, p.endMin);
1594
+ if (e - s >= MIN_EVENT_MINUTES) onTimeRangeSelect(currentDate, s, e);
1595
+ }
1596
+ createRef.current = null;
1597
+ createPreviewRef.current = null;
1598
+ setCreatePreview(null);
1599
+ }
1600
+ if (moveRef.current?.moved) {
1601
+ const mv = moveRef.current;
1602
+ const p = livePreviewRef.current;
1603
+ if (p && Number.isFinite(p.startMin) && Number.isFinite(p.endMin)) {
1604
+ justMovedRef.current = true;
1605
+ onEventTimeChange(mv.event, p.startMin, p.endMin);
1606
+ }
1607
+ }
1608
+ moveRef.current = null;
1609
+ livePreviewRef.current = null;
1610
+ setLivePreview(null);
1611
+ setInteracting(false);
1612
+ }
1613
+ window.addEventListener("pointermove", onMove);
1614
+ window.addEventListener("pointerup", onUp);
1615
+ return () => {
1616
+ window.removeEventListener("pointermove", onMove);
1617
+ window.removeEventListener("pointerup", onUp);
1618
+ };
1619
+ }, [interacting, currentDate, onTimeRangeSelect, onEventTimeChange]);
1620
+ const beginCreate = (e) => {
1621
+ if (e.target.closest('[data-event="true"]')) return;
1622
+ if (e.pointerType === "touch") return;
1623
+ const rect = colRef.current?.getBoundingClientRect();
1624
+ if (!rect) return;
1625
+ const m = snapMinutes(minutesFromY(e.clientY, rect));
1626
+ createRef.current = { startMin: m };
1627
+ setCreatePreview({ startMin: m, endMin: m });
1628
+ setInteracting(true);
1629
+ };
1630
+ const beginMove = (e, event, mode) => {
1631
+ if ((event.recurrence || event.isRecurringInstance) && mode !== "move") return;
1632
+ e.stopPropagation();
1633
+ e.preventDefault();
1634
+ const origStart = hhmmToMinutes(event.startTime);
1635
+ const origEnd = event.endTime ? hhmmToMinutes(event.endTime) : origStart + 60;
1636
+ moveRef.current = { event, mode, startY: e.clientY, origStart, origEnd, moved: false };
1637
+ setInteracting(true);
1638
+ };
1639
+ const handleEventItemClick = (e, event) => {
1640
+ e.stopPropagation();
1641
+ if (justMovedRef.current) {
1642
+ justMovedRef.current = false;
1643
+ return;
1644
+ }
1645
+ onEventClick(event, e.currentTarget);
1646
+ };
1647
+ const [focusedHour, setFocusedHour] = useState(null);
1648
+ const slotRefs = useRef([]);
1649
+ useEffect(() => {
1650
+ if (focusedHour == null) return;
1651
+ slotRefs.current[focusedHour]?.focus();
1652
+ }, [focusedHour]);
1653
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-timegrid sft-cal-timegrid--day", children: [
1654
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__corner" }),
1655
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__colhead", children: /* @__PURE__ */ jsxs(
1656
+ "span",
1657
+ {
1658
+ className: dayCls ? `sft-cal-timegrid__daylabel ${dayCls}` : "sft-cal-timegrid__daylabel",
1659
+ children: [
1660
+ (dayNames[language] ?? dayNames.en ?? [])[dow],
1661
+ " (",
1662
+ currentDate.getDate(),
1663
+ ")"
1664
+ ]
1665
+ }
1666
+ ) }),
1667
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__hours", children: HOURS.map((hour) => /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__hour", children: /* @__PURE__ */ jsxs("span", { className: "sft-cal-timegrid__hourlabel", children: [
1668
+ hour.toString().padStart(2, "0"),
1669
+ ":00"
1670
+ ] }) }, hour)) }),
1671
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-timegrid__col", ref: colRef, onPointerDown: beginCreate, children: [
1672
+ HOURS.map((hour) => /* @__PURE__ */ jsx(
1673
+ "button",
1674
+ {
1675
+ ref: (el) => {
1676
+ slotRefs.current[hour] = el;
1677
+ },
1678
+ type: "button",
1679
+ className: "sft-cal-timegrid__slot",
1680
+ tabIndex: (focusedHour ?? 9) === hour ? 0 : -1,
1681
+ "aria-label": `${hour.toString().padStart(2, "0")}:00`,
1682
+ onFocus: () => setFocusedHour(hour),
1683
+ onKeyDown: (e) => {
1684
+ if (e.key === "ArrowDown") {
1685
+ e.preventDefault();
1686
+ setFocusedHour(Math.min(23, hour + 1));
1687
+ } else if (e.key === "ArrowUp") {
1688
+ e.preventDefault();
1689
+ setFocusedHour(Math.max(0, hour - 1));
1690
+ }
1691
+ },
1692
+ onClick: (e) => {
1693
+ if (e.target.closest('[data-event="true"]')) return;
1694
+ onAddEventClick(currentDate, hour, e.clientX, e.clientY);
1695
+ }
1696
+ },
1697
+ hour
1698
+ )),
1699
+ dayEvents.map((event) => {
1700
+ const isEditing = selectedEvent?.id === event.id && previewEvent;
1701
+ const live = livePreview?.id === event.id ? livePreview : null;
1702
+ const data = isEditing ? {
1703
+ title: previewEvent.title,
1704
+ startTime: previewEvent.startTime,
1705
+ endTime: previewEvent.endTime,
1706
+ categoryId: previewEvent.categoryId
1707
+ } : event;
1708
+ const color = categories.find((c) => c.id === data.categoryId)?.color || "#E30000";
1709
+ const startMin = live ? live.startMin : hhmmToMinutes(data.startTime);
1710
+ const endMin = live ? live.endMin : data.endTime ? hhmmToMinutes(data.endTime) : startMin + 60;
1711
+ const top = startMin / (24 * 60) * 100;
1712
+ const height = (endMin - startMin) / (24 * 60) * 100;
1713
+ const resizable = !event.recurrence && !event.isRecurringInstance;
1714
+ return /* @__PURE__ */ jsxs(
1715
+ "div",
1716
+ {
1717
+ "data-event": "true",
1718
+ "data-movable": "true",
1719
+ className: "sft-cal-timeevent",
1720
+ "data-editing": isEditing || void 0,
1721
+ role: "button",
1722
+ tabIndex: 0,
1723
+ "aria-label": data.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)"),
1724
+ style: {
1725
+ top: `${top}%`,
1726
+ height: `${height}%`,
1727
+ left: "8px",
1728
+ width: "calc(100% - 16px)",
1729
+ background: withAlpha(color, 0.12),
1730
+ borderLeft: `3px solid ${color}`
1731
+ },
1732
+ onPointerDown: (e) => beginMove(e, event, "move"),
1733
+ onClick: (e) => handleEventItemClick(e, event),
1734
+ onKeyDown: (e) => {
1735
+ if (e.key === "Enter" || e.key === " ") {
1736
+ e.preventDefault();
1737
+ e.stopPropagation();
1738
+ onEventClick(event, e.currentTarget);
1739
+ }
1740
+ },
1741
+ children: [
1742
+ resizable && /* @__PURE__ */ jsx(
1743
+ "span",
1744
+ {
1745
+ className: "sft-cal-timeevent__resize sft-cal-timeevent__resize--top",
1746
+ onPointerDown: (e) => beginMove(e, event, "resize-start")
1747
+ }
1748
+ ),
1749
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-timeevent__label", style: { color }, children: [
1750
+ event.recurrence && /* @__PURE__ */ jsx(Repeat, { size: 12 }),
1751
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-timeevent__title", children: data.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)") })
1752
+ ] }),
1753
+ resizable && /* @__PURE__ */ jsx(
1754
+ "span",
1755
+ {
1756
+ className: "sft-cal-timeevent__resize sft-cal-timeevent__resize--bottom",
1757
+ onPointerDown: (e) => beginMove(e, event, "resize-end")
1758
+ }
1759
+ )
1760
+ ]
1761
+ },
1762
+ event.id
1763
+ );
1764
+ }),
1765
+ createPreview && (() => {
1766
+ const s = Math.min(createPreview.startMin, createPreview.endMin);
1767
+ const e = Math.max(createPreview.startMin, createPreview.endMin);
1768
+ const top = s / (24 * 60) * 100;
1769
+ const height = (e - s) / (24 * 60) * 100;
1770
+ return /* @__PURE__ */ jsx(
1771
+ "div",
1772
+ {
1773
+ className: "sft-cal-timeevent sft-cal-timeevent--draft",
1774
+ style: {
1775
+ top: `${top}%`,
1776
+ height: `${height}%`,
1777
+ left: "8px",
1778
+ width: "calc(100% - 16px)"
1779
+ },
1780
+ children: /* @__PURE__ */ jsx("div", { className: "sft-cal-timeevent__label", children: /* @__PURE__ */ jsxs("span", { className: "sft-cal-timeevent__title", children: [
1781
+ minutesToHHMM(s),
1782
+ "\u2013",
1783
+ minutesToHHMM(e)
1784
+ ] }) })
1785
+ }
1786
+ );
1787
+ })()
1788
+ ] })
1789
+ ] });
1790
+ }
1791
+ var startOfDay2 = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
1792
+ var EVENT_H = 22;
1793
+ var ROW = EVENT_H + 1;
1794
+ function assignLayers(spanning) {
1795
+ const layers = /* @__PURE__ */ new Map();
1796
+ const laneEnds = [];
1797
+ const sorted = [...spanning].sort((a, b) => a.start.getTime() - b.start.getTime());
1798
+ for (const ev of sorted) {
1799
+ let lane = 0;
1800
+ while (lane < laneEnds.length && laneEnds[lane] >= startOfDay2(ev.start)) lane++;
1801
+ laneEnds[lane] = startOfDay2(ev.end);
1802
+ layers.set(ev.id, lane);
1803
+ }
1804
+ return layers;
1805
+ }
1806
+ function formatTime(time, language) {
1807
+ const h = Number(time.split(":")[0]) || 0;
1808
+ const period = h < 12 ? pick(language, "\uC624\uC804", "AM") : pick(language, "\uC624\uD6C4", "PM");
1809
+ const display = h % 12 === 0 ? 12 : h % 12;
1810
+ return pick(language, `${period} ${display}\uC2DC`, `${display} ${period}`);
1811
+ }
1812
+ function MonthView({
1813
+ currentDate,
1814
+ events,
1815
+ selectedCategoryIds,
1816
+ categories,
1817
+ language,
1818
+ holidays = [],
1819
+ selectedEvent,
1820
+ previewEvent,
1821
+ expandedRows,
1822
+ setExpandedRows,
1823
+ onEventClick,
1824
+ onAddEventClick,
1825
+ onRangeSelect,
1826
+ onEventMove,
1827
+ onEventResize
1828
+ }) {
1829
+ const { days, rows } = getDaysInMonth(currentDate);
1830
+ const maxEventsToShow = rows === 4 ? 8 : rows === 5 ? 6 : 5;
1831
+ const rangeStart = startOfDay2(days[0]);
1832
+ const rangeEnd = startOfDay2(days[days.length - 1]);
1833
+ const expanded = expandRecurringEvents(events, rangeStart, rangeEnd).filter(
1834
+ (e) => !e.categoryId || selectedCategoryIds.includes(e.categoryId)
1835
+ );
1836
+ const [dragStart, setDragStart] = useState(null);
1837
+ const [dragEnd, setDragEnd] = useState(null);
1838
+ const [dragging, setDragging] = useState(false);
1839
+ const dragXY = useRef({ x: 0, y: 0 });
1840
+ useEffect(() => {
1841
+ if (!dragging) return;
1842
+ const onUp = () => {
1843
+ setDragging(false);
1844
+ if (dragStart && dragEnd && !isSameDay(dragStart, dragEnd)) {
1845
+ const [s, e] = dragStart <= dragEnd ? [dragStart, dragEnd] : [dragEnd, dragStart];
1846
+ onRangeSelect(s, e, dragXY.current.x, dragXY.current.y);
1847
+ }
1848
+ setDragStart(null);
1849
+ setDragEnd(null);
1850
+ };
1851
+ window.addEventListener("pointerup", onUp);
1852
+ return () => window.removeEventListener("pointerup", onUp);
1853
+ }, [dragging, dragStart, dragEnd, onRangeSelect]);
1854
+ const moveRef = useRef(null);
1855
+ const justMovedRef = useRef(false);
1856
+ const [moveTargetDate, setMoveTargetDate] = useState(null);
1857
+ const moveTargetDateRef = useRef(null);
1858
+ useEffect(() => {
1859
+ function onMove(e) {
1860
+ const m = moveRef.current;
1861
+ if (!m || m.moved) return;
1862
+ if (Math.abs(e.clientX - m.startX) > 4 || Math.abs(e.clientY - m.startY) > 4) m.moved = true;
1863
+ }
1864
+ function onUp() {
1865
+ const m = moveRef.current;
1866
+ if (m?.moved) {
1867
+ const target = moveTargetDateRef.current;
1868
+ if (target) {
1869
+ justMovedRef.current = true;
1870
+ if (m.mode === "move") onEventMove(m.event, target);
1871
+ else onEventResize(m.event, m.mode === "resize-start" ? "start" : "end", target);
1872
+ }
1873
+ }
1874
+ moveRef.current = null;
1875
+ moveTargetDateRef.current = null;
1876
+ setMoveTargetDate(null);
1877
+ }
1878
+ window.addEventListener("pointermove", onMove);
1879
+ window.addEventListener("pointerup", onUp);
1880
+ return () => {
1881
+ window.removeEventListener("pointermove", onMove);
1882
+ window.removeEventListener("pointerup", onUp);
1883
+ };
1884
+ }, [onEventMove, onEventResize]);
1885
+ const startEventMove = (e, event, mode = "move") => {
1886
+ if (e.pointerType === "touch") return;
1887
+ if ((event.recurrence || event.isRecurringInstance) && mode !== "move") return;
1888
+ e.stopPropagation();
1889
+ moveRef.current = { event, mode, startX: e.clientX, startY: e.clientY, moved: false };
1890
+ };
1891
+ const handleEventItemClick = (e, event) => {
1892
+ e.stopPropagation();
1893
+ if (justMovedRef.current) {
1894
+ justMovedRef.current = false;
1895
+ return;
1896
+ }
1897
+ onEventClick(event, e.currentTarget);
1898
+ };
1899
+ const [focusedIndex, setFocusedIndex] = useState(null);
1900
+ const cellRefs = useRef([]);
1901
+ useEffect(() => {
1902
+ if (focusedIndex == null) return;
1903
+ cellRefs.current[focusedIndex]?.focus();
1904
+ }, [focusedIndex]);
1905
+ const handleGridKeyDown = (e) => {
1906
+ const base = focusedIndex ?? days.findIndex((d) => isToday(d)) ?? 0;
1907
+ let next = base;
1908
+ if (e.key === "ArrowRight") next = base + 1;
1909
+ else if (e.key === "ArrowLeft") next = base - 1;
1910
+ else if (e.key === "ArrowDown") next = base + 7;
1911
+ else if (e.key === "ArrowUp") next = base - 7;
1912
+ else if (e.key === "Enter" || e.key === " ") {
1913
+ const d = days[base];
1914
+ if (d) onAddEventClick(d);
1915
+ e.preventDefault();
1916
+ return;
1917
+ } else return;
1918
+ e.preventDefault();
1919
+ setFocusedIndex(Math.max(0, Math.min(days.length - 1, next)));
1920
+ };
1921
+ const dragPreview = dragging && dragStart && dragEnd && !isSameDay(dragStart, dragEnd) ? dragStart <= dragEnd ? { start: dragStart, end: dragEnd } : { start: dragEnd, end: dragStart } : null;
1922
+ const inRange = (date, s, e) => {
1923
+ const t = startOfDay2(date).getTime();
1924
+ return t >= startOfDay2(s).getTime() && t <= startOfDay2(e).getTime();
1925
+ };
1926
+ const spanning = expanded.filter((e) => e.endDate).map((e) => ({ id: e.id, start: e.date, end: e.endDate }));
1927
+ if (previewEvent?.isPeriod && previewEvent.startDate && previewEvent.endDate) {
1928
+ spanning.push({ id: "preview-temp", start: previewEvent.startDate, end: previewEvent.endDate });
1929
+ }
1930
+ if (dragPreview) {
1931
+ spanning.push({ id: "drag-temp", start: dragPreview.start, end: dragPreview.end });
1932
+ }
1933
+ const layers = assignLayers(spanning);
1934
+ const maxLayer = layers.size ? Math.max(...layers.values()) : -1;
1935
+ const periodEventsHeight = (maxLayer + 1) * ROW;
1936
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-month", children: [
1937
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-month__weekdays", children: (dayNames[language] ?? dayNames.en ?? []).map((label, i) => /* @__PURE__ */ jsx(
1938
+ "div",
1939
+ {
1940
+ className: `sft-cal-month__weekday${i === 0 ? " sft-cal-sun" : i === 6 ? " sft-cal-sat" : ""}`,
1941
+ children: label
1942
+ },
1943
+ label
1944
+ )) }),
1945
+ /* @__PURE__ */ jsx(
1946
+ "div",
1947
+ {
1948
+ className: "sft-cal-month__grid",
1949
+ style: { gridTemplateRows: `repeat(${rows}, 1fr)` },
1950
+ role: "grid",
1951
+ "aria-label": pick(language, "\uC6D4\uAC04 \uCE98\uB9B0\uB354", "Month calendar"),
1952
+ onKeyDown: handleGridKeyDown,
1953
+ children: days.map((date, index) => {
1954
+ const rowIndex = Math.floor(index / 7);
1955
+ const isWeekend = index % 7 === 0 || index % 7 === 6;
1956
+ const dow = date.getDay();
1957
+ const dayEvents = expanded.filter((e) => {
1958
+ const t = startOfDay2(date).getTime();
1959
+ const s = startOfDay2(e.date).getTime();
1960
+ const en = e.endDate ? startOfDay2(e.endDate).getTime() : s;
1961
+ return t >= s && t <= en;
1962
+ });
1963
+ const singles = dayEvents.filter((e) => !e.endDate);
1964
+ const periodsForDay = dayEvents.filter((e) => e.endDate);
1965
+ const rowExpanded = expandedRows.has(rowIndex);
1966
+ const effectiveMax = rowExpanded ? dayEvents.length : maxEventsToShow;
1967
+ const maxSingles = Math.max(0, effectiveMax - periodsForDay.length);
1968
+ let displaySingles = singles.slice(0, maxSingles);
1969
+ let remaining = singles.length - displaySingles.length;
1970
+ if (!rowExpanded && remaining > 0 && maxSingles > 0) {
1971
+ displaySingles = singles.slice(0, maxSingles - 1);
1972
+ remaining = singles.length - displaySingles.length;
1973
+ }
1974
+ const holiday = holidays.find((h) => isSameDay(h.date, date));
1975
+ const dateNumCls = !isCurrentMonth(date, currentDate) ? "sft-cal-month__num sft-cal-month__num--dim" : isToday(date) ? "sft-cal-month__num sft-cal-month__num--today" : dow === 0 || holiday ? "sft-cal-month__num sft-cal-sun" : dow === 6 ? "sft-cal-month__num sft-cal-sat" : "sft-cal-month__num";
1976
+ return /* @__PURE__ */ jsxs(
1977
+ "div",
1978
+ {
1979
+ ref: (el) => {
1980
+ cellRefs.current[index] = el;
1981
+ },
1982
+ className: "sft-cal-month__cell",
1983
+ "data-weekend": isWeekend || void 0,
1984
+ "data-lastrow": index >= days.length - 7 || void 0,
1985
+ "data-lastcol": index % 7 === 6 || void 0,
1986
+ "data-movetarget": moveTargetDate && isSameDay(moveTargetDate, date) || void 0,
1987
+ role: "gridcell",
1988
+ tabIndex: (focusedIndex ?? days.findIndex((d) => isToday(d)) ?? 0) === index ? 0 : -1,
1989
+ "aria-label": `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}${holiday ? ` ${holiday.name}` : ""}`,
1990
+ "aria-current": isToday(date) ? "date" : void 0,
1991
+ onFocus: () => setFocusedIndex(index),
1992
+ onPointerDown: (e) => {
1993
+ if (e.target.closest('[data-event="true"],button')) return;
1994
+ if (e.pointerType === "touch") return;
1995
+ dragXY.current = { x: e.clientX, y: e.clientY };
1996
+ setDragging(true);
1997
+ setDragStart(date);
1998
+ setDragEnd(date);
1999
+ },
2000
+ onMouseEnter: () => {
2001
+ if (dragging) setDragEnd(date);
2002
+ if (moveRef.current) {
2003
+ moveTargetDateRef.current = date;
2004
+ setMoveTargetDate(date);
2005
+ }
2006
+ },
2007
+ children: [
2008
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-month__cellhead", children: [
2009
+ holiday && /* @__PURE__ */ jsx("span", { className: "sft-cal-month__holiday", children: holiday.name }),
2010
+ /* @__PURE__ */ jsx("span", { className: dateNumCls, children: date.getDate() })
2011
+ ] }),
2012
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-month__events", children: [
2013
+ periodsForDay.map((event) => {
2014
+ const layer = layers.get(event.id) ?? 0;
2015
+ const color = categories.find((c) => c.id === event.categoryId)?.color || "#E30000";
2016
+ const isStart = isSameDay(event.date, date);
2017
+ const isEnd = event.endDate ? isSameDay(event.endDate, date) : true;
2018
+ const firstOfWeek = index % 7 === 0;
2019
+ const lastOfWeek = index % 7 === 6;
2020
+ const roundLeft = isStart || firstOfWeek;
2021
+ const roundRight = isEnd || lastOfWeek;
2022
+ const resizable = !event.recurrence && !event.isRecurringInstance;
2023
+ return /* @__PURE__ */ jsxs(
2024
+ "div",
2025
+ {
2026
+ "data-event": "true",
2027
+ className: "sft-cal-month__bar",
2028
+ "data-movable": "true",
2029
+ role: "button",
2030
+ tabIndex: isStart || firstOfWeek ? 0 : -1,
2031
+ "aria-label": event.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)"),
2032
+ onKeyDown: (e) => {
2033
+ if (e.key === "Enter" || e.key === " ") {
2034
+ e.preventDefault();
2035
+ e.stopPropagation();
2036
+ onEventClick(event, e.currentTarget);
2037
+ }
2038
+ },
2039
+ style: {
2040
+ top: `${layer * ROW}px`,
2041
+ background: withAlpha(color, 0.1),
2042
+ borderTopLeftRadius: roundLeft ? 4 : 0,
2043
+ borderBottomLeftRadius: roundLeft ? 4 : 0,
2044
+ borderTopRightRadius: roundRight ? 4 : 0,
2045
+ borderBottomRightRadius: roundRight ? 4 : 0,
2046
+ paddingLeft: roundLeft ? 8 : 4,
2047
+ paddingRight: roundRight ? 8 : 4
2048
+ },
2049
+ onPointerDown: (e) => startEventMove(e, event),
2050
+ onClick: (e) => handleEventItemClick(e, event),
2051
+ children: [
2052
+ resizable && isStart && /* @__PURE__ */ jsx(
2053
+ "span",
2054
+ {
2055
+ className: "sft-cal-month__bar-resize sft-cal-month__bar-resize--left",
2056
+ onPointerDown: (e) => startEventMove(e, event, "resize-start")
2057
+ }
2058
+ ),
2059
+ (isStart || firstOfWeek) && /* @__PURE__ */ jsxs("span", { className: "sft-cal-month__bar-title", style: { color }, children: [
2060
+ event.recurrence && /* @__PURE__ */ jsx(Repeat, { size: 11 }),
2061
+ event.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)")
2062
+ ] }),
2063
+ resizable && isEnd && /* @__PURE__ */ jsx(
2064
+ "span",
2065
+ {
2066
+ className: "sft-cal-month__bar-resize sft-cal-month__bar-resize--right",
2067
+ onPointerDown: (e) => startEventMove(e, event, "resize-end")
2068
+ }
2069
+ )
2070
+ ]
2071
+ },
2072
+ event.id
2073
+ );
2074
+ }),
2075
+ dragPreview && inRange(date, dragPreview.start, dragPreview.end) && (() => {
2076
+ const layer = layers.get("drag-temp") ?? 0;
2077
+ const isStart = isSameDay(dragPreview.start, date);
2078
+ const isEnd = isSameDay(dragPreview.end, date);
2079
+ const firstOfWeek = index % 7 === 0;
2080
+ const lastOfWeek = index % 7 === 6;
2081
+ const roundLeft = isStart || firstOfWeek;
2082
+ const roundRight = isEnd || lastOfWeek;
2083
+ return /* @__PURE__ */ jsx(
2084
+ "div",
2085
+ {
2086
+ className: "sft-cal-month__bar sft-cal-month__bar--drag",
2087
+ style: {
2088
+ top: `${layer * ROW}px`,
2089
+ borderTopLeftRadius: roundLeft ? 4 : 0,
2090
+ borderBottomLeftRadius: roundLeft ? 4 : 0,
2091
+ borderTopRightRadius: roundRight ? 4 : 0,
2092
+ borderBottomRightRadius: roundRight ? 4 : 0,
2093
+ paddingLeft: roundLeft ? 8 : 4,
2094
+ paddingRight: roundRight ? 8 : 4
2095
+ },
2096
+ children: (isStart || firstOfWeek) && /* @__PURE__ */ jsxs("span", { className: "sft-cal-month__bar-title", children: [
2097
+ /* @__PURE__ */ jsx(Plus, { size: 12 }),
2098
+ pick(language, "\uC0C8 \uC77C\uC815", "New event")
2099
+ ] })
2100
+ }
2101
+ );
2102
+ })(),
2103
+ displaySingles.map((event, i) => {
2104
+ const color = categories.find((c) => c.id === event.categoryId)?.color || "#E30000";
2105
+ return /* @__PURE__ */ jsxs(
2106
+ "div",
2107
+ {
2108
+ "data-event": "true",
2109
+ className: "sft-cal-month__chip",
2110
+ "data-movable": "true",
2111
+ role: "button",
2112
+ tabIndex: 0,
2113
+ "aria-label": event.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)"),
2114
+ style: { marginTop: i === 0 ? periodEventsHeight : 1 },
2115
+ onPointerDown: (e) => startEventMove(e, event),
2116
+ onClick: (e) => handleEventItemClick(e, event),
2117
+ onKeyDown: (e) => {
2118
+ if (e.key === "Enter" || e.key === " ") {
2119
+ e.preventDefault();
2120
+ e.stopPropagation();
2121
+ onEventClick(event, e.currentTarget);
2122
+ }
2123
+ },
2124
+ children: [
2125
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-month__chip-bar", style: { background: color } }),
2126
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-month__chip-title", children: event.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)") }),
2127
+ event.recurrence && /* @__PURE__ */ jsx(Repeat, { className: "sft-cal-month__chip-icon", size: 12 }),
2128
+ event.startTime && /* @__PURE__ */ jsx("span", { className: "sft-cal-month__chip-time", children: formatTime(event.startTime, language) })
2129
+ ]
2130
+ },
2131
+ event.id
2132
+ );
2133
+ }),
2134
+ remaining > 0 && /* @__PURE__ */ jsx(
2135
+ "button",
2136
+ {
2137
+ type: "button",
2138
+ className: "sft-cal-month__more",
2139
+ style: { marginTop: displaySingles.length ? 1 : periodEventsHeight },
2140
+ onClick: (e) => {
2141
+ e.stopPropagation();
2142
+ setExpandedRows((prev) => {
2143
+ const next = new Set(prev);
2144
+ if (next.has(rowIndex)) next.delete(rowIndex);
2145
+ else next.add(rowIndex);
2146
+ return next;
2147
+ });
2148
+ },
2149
+ children: pick(language, `+${remaining}\uAC1C`, `+${remaining} more`)
2150
+ }
2151
+ ),
2152
+ previewEvent && !previewEvent.isPeriod && isSameDay(previewEvent.startDate ?? currentDate, date) && /* @__PURE__ */ jsxs(
2153
+ "div",
2154
+ {
2155
+ className: "sft-cal-month__chip sft-cal-month__chip--preview",
2156
+ style: { marginTop: singles.length === 0 ? periodEventsHeight : 1 },
2157
+ children: [
2158
+ /* @__PURE__ */ jsx(
2159
+ "span",
2160
+ {
2161
+ className: "sft-cal-month__chip-bar",
2162
+ style: {
2163
+ background: categories.find((c) => c.id === previewEvent.categoryId)?.color || "#000"
2164
+ }
2165
+ }
2166
+ ),
2167
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-month__chip-title", children: previewEvent.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)") }),
2168
+ previewEvent.startTime && /* @__PURE__ */ jsx("span", { className: "sft-cal-month__chip-time", children: formatTime(previewEvent.startTime, language) })
2169
+ ]
2170
+ }
2171
+ ),
2172
+ /* @__PURE__ */ jsxs(
2173
+ "button",
2174
+ {
2175
+ type: "button",
2176
+ className: "sft-cal-month__add",
2177
+ style: { marginTop: singles.length === 0 ? periodEventsHeight : 1 },
2178
+ onClick: (e) => {
2179
+ e.stopPropagation();
2180
+ onAddEventClick(date, e.clientX, e.clientY);
2181
+ },
2182
+ children: [
2183
+ /* @__PURE__ */ jsx(Plus, { size: 14 }),
2184
+ pick(language, "\uC0C8 \uC77C\uC815", "New event")
2185
+ ]
2186
+ }
2187
+ )
2188
+ ] })
2189
+ ]
2190
+ },
2191
+ date.toISOString()
2192
+ );
2193
+ })
2194
+ }
2195
+ )
2196
+ ] });
2197
+ }
2198
+ var HOURS2 = Array.from({ length: 24 }, (_, i) => i);
2199
+ var MIN_EVENT_MINUTES2 = 15;
2200
+ function dayColorClass(dayOfWeek) {
2201
+ if (dayOfWeek === 0) return "sft-cal-sun";
2202
+ if (dayOfWeek === 6) return "sft-cal-sat";
2203
+ return void 0;
2204
+ }
2205
+ function WeekView({
2206
+ currentDate,
2207
+ events,
2208
+ selectedCategoryIds,
2209
+ categories,
2210
+ language,
2211
+ selectedEvent,
2212
+ previewEvent,
2213
+ onEventClick,
2214
+ onAddEventClick,
2215
+ onTimeRangeSelect,
2216
+ onEventTimeChange
2217
+ }) {
2218
+ const weekDays = getWeekDays(currentDate);
2219
+ const weekStart = weekDays[0];
2220
+ const weekEnd = weekDays[weekDays.length - 1];
2221
+ const expanded = expandRecurringEvents(events, weekStart, weekEnd);
2222
+ const eventsForDay = (date) => expanded.filter((event) => {
2223
+ const startMatch = event.date.getDate() === date.getDate() && event.date.getMonth() === date.getMonth() && event.date.getFullYear() === date.getFullYear();
2224
+ let inRange = startMatch;
2225
+ if (!inRange && event.endDate) {
2226
+ const check = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
2227
+ const s = new Date(
2228
+ event.date.getFullYear(),
2229
+ event.date.getMonth(),
2230
+ event.date.getDate()
2231
+ ).getTime();
2232
+ const e = new Date(
2233
+ event.endDate.getFullYear(),
2234
+ event.endDate.getMonth(),
2235
+ event.endDate.getDate()
2236
+ ).getTime();
2237
+ inRange = check >= s && check <= e;
2238
+ }
2239
+ return inRange;
2240
+ }).filter((event) => !event.categoryId || selectedCategoryIds.includes(event.categoryId));
2241
+ const colRefs = useRef([]);
2242
+ const createRef = useRef(null);
2243
+ const [createPreview, setCreatePreview] = useState(null);
2244
+ const moveRef = useRef(null);
2245
+ const justMovedRef = useRef(false);
2246
+ const [livePreview, setLivePreview] = useState(null);
2247
+ const createPreviewRef = useRef(
2248
+ null
2249
+ );
2250
+ const livePreviewRef = useRef(null);
2251
+ const [moveDayIndex, setMoveDayIndex] = useState(null);
2252
+ const [interacting, setInteracting] = useState(false);
2253
+ const findColIndexAtX = (x) => {
2254
+ for (let i = 0; i < colRefs.current.length; i++) {
2255
+ const el = colRefs.current[i];
2256
+ if (!el) continue;
2257
+ const r = el.getBoundingClientRect();
2258
+ if (x >= r.left && x <= r.right) return i;
2259
+ }
2260
+ return null;
2261
+ };
2262
+ useEffect(() => {
2263
+ if (!interacting) return;
2264
+ function onMove(e) {
2265
+ if (createRef.current) {
2266
+ const rect = colRefs.current[createRef.current.dayIndex]?.getBoundingClientRect();
2267
+ if (!rect || rect.height <= 0) return;
2268
+ const m = snapMinutes(minutesFromY(e.clientY, rect));
2269
+ const next = {
2270
+ dayIndex: createRef.current.dayIndex,
2271
+ startMin: createRef.current.startMin,
2272
+ endMin: m
2273
+ };
2274
+ createPreviewRef.current = next;
2275
+ setCreatePreview(next);
2276
+ return;
2277
+ }
2278
+ const mv = moveRef.current;
2279
+ if (mv) {
2280
+ if (mv.mode === "move") {
2281
+ const hoverIdx = findColIndexAtX(e.clientX);
2282
+ if (hoverIdx != null && hoverIdx !== mv.dayIndex) {
2283
+ mv.dayIndex = hoverIdx;
2284
+ setMoveDayIndex(hoverIdx);
2285
+ }
2286
+ }
2287
+ const rect = colRefs.current[mv.dayIndex]?.getBoundingClientRect();
2288
+ if (!rect || rect.height <= 0) return;
2289
+ if (!mv.moved && Math.abs(e.clientY - mv.startY) > 4) mv.moved = true;
2290
+ if (!mv.moved) return;
2291
+ const deltaMin = snapMinutes((e.clientY - mv.startY) / rect.height * 24 * 60);
2292
+ let newStart = mv.origStart;
2293
+ let newEnd = mv.origEnd;
2294
+ if (mv.mode === "move") {
2295
+ newStart = mv.origStart + deltaMin;
2296
+ newEnd = mv.origEnd + deltaMin;
2297
+ if (newStart < 0) {
2298
+ newEnd -= newStart;
2299
+ newStart = 0;
2300
+ }
2301
+ if (newEnd > 24 * 60) {
2302
+ newStart -= newEnd - 24 * 60;
2303
+ newEnd = 24 * 60;
2304
+ }
2305
+ } else if (mv.mode === "resize-start") {
2306
+ newStart = Math.max(0, Math.min(mv.origEnd - MIN_EVENT_MINUTES2, mv.origStart + deltaMin));
2307
+ } else {
2308
+ newEnd = Math.min(
2309
+ 24 * 60,
2310
+ Math.max(mv.origStart + MIN_EVENT_MINUTES2, mv.origEnd + deltaMin)
2311
+ );
2312
+ }
2313
+ const next = { id: mv.event.id, startMin: newStart, endMin: newEnd };
2314
+ livePreviewRef.current = next;
2315
+ setLivePreview(next);
2316
+ }
2317
+ }
2318
+ function onUp() {
2319
+ if (createRef.current) {
2320
+ const dayIndex = createRef.current.dayIndex;
2321
+ const day = weekDays[dayIndex];
2322
+ const p = createPreviewRef.current;
2323
+ if (p && day && Number.isFinite(p.startMin) && Number.isFinite(p.endMin)) {
2324
+ const s = Math.min(p.startMin, p.endMin);
2325
+ const e = Math.max(p.startMin, p.endMin);
2326
+ if (e - s >= MIN_EVENT_MINUTES2) onTimeRangeSelect(day, s, e);
2327
+ }
2328
+ createRef.current = null;
2329
+ createPreviewRef.current = null;
2330
+ setCreatePreview(null);
2331
+ }
2332
+ if (moveRef.current?.moved) {
2333
+ const mv = moveRef.current;
2334
+ const p = livePreviewRef.current;
2335
+ if (p && Number.isFinite(p.startMin) && Number.isFinite(p.endMin)) {
2336
+ justMovedRef.current = true;
2337
+ const newDay = weekDays[mv.dayIndex];
2338
+ const dayChanged = newDay && !isSameDay(newDay, mv.event.date);
2339
+ onEventTimeChange(mv.event, p.startMin, p.endMin, dayChanged ? newDay : void 0);
2340
+ }
2341
+ }
2342
+ moveRef.current = null;
2343
+ livePreviewRef.current = null;
2344
+ setLivePreview(null);
2345
+ setMoveDayIndex(null);
2346
+ setInteracting(false);
2347
+ }
2348
+ window.addEventListener("pointermove", onMove);
2349
+ window.addEventListener("pointerup", onUp);
2350
+ return () => {
2351
+ window.removeEventListener("pointermove", onMove);
2352
+ window.removeEventListener("pointerup", onUp);
2353
+ };
2354
+ }, [interacting, onTimeRangeSelect, onEventTimeChange]);
2355
+ const beginCreate = (e, dayIndex) => {
2356
+ if (e.target.closest('[data-event="true"]')) return;
2357
+ if (e.pointerType === "touch") return;
2358
+ const rect = colRefs.current[dayIndex]?.getBoundingClientRect();
2359
+ if (!rect) return;
2360
+ const m = snapMinutes(minutesFromY(e.clientY, rect));
2361
+ createRef.current = { dayIndex, startMin: m };
2362
+ setCreatePreview({ dayIndex, startMin: m, endMin: m });
2363
+ setInteracting(true);
2364
+ };
2365
+ const beginMove = (e, event, dayIndex, mode) => {
2366
+ if ((event.recurrence || event.isRecurringInstance) && mode !== "move") return;
2367
+ e.stopPropagation();
2368
+ e.preventDefault();
2369
+ const origStart = hhmmToMinutes(event.startTime);
2370
+ const origEnd = event.endTime ? hhmmToMinutes(event.endTime) : origStart + 60;
2371
+ moveRef.current = {
2372
+ event,
2373
+ dayIndex,
2374
+ mode,
2375
+ startY: e.clientY,
2376
+ origStart,
2377
+ origEnd,
2378
+ moved: false
2379
+ };
2380
+ setInteracting(true);
2381
+ };
2382
+ const handleEventItemClick = (e, event) => {
2383
+ e.stopPropagation();
2384
+ if (justMovedRef.current) {
2385
+ justMovedRef.current = false;
2386
+ return;
2387
+ }
2388
+ onEventClick(event, e.currentTarget);
2389
+ };
2390
+ const [focusedCell, setFocusedCell] = useState(null);
2391
+ const slotRefs = useRef({});
2392
+ useEffect(() => {
2393
+ if (!focusedCell) return;
2394
+ slotRefs.current[`${focusedCell.dayIndex}-${focusedCell.hour}`]?.focus();
2395
+ }, [focusedCell]);
2396
+ const defaultCell = { dayIndex: Math.max(0, weekDays.findIndex((d) => isToday(d))), hour: 9 };
2397
+ const activeCell = focusedCell ?? defaultCell;
2398
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-timegrid sft-cal-timegrid--week", children: [
2399
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__corner" }),
2400
+ weekDays.map((day, dayIndex) => {
2401
+ const cls = dayColorClass(day.getDay());
2402
+ return /* @__PURE__ */ jsx(
2403
+ "div",
2404
+ {
2405
+ className: "sft-cal-timegrid__colhead",
2406
+ "data-movetarget": moveDayIndex === dayIndex || void 0,
2407
+ children: /* @__PURE__ */ jsxs(
2408
+ "span",
2409
+ {
2410
+ className: cls ? `sft-cal-timegrid__daylabel ${cls}` : "sft-cal-timegrid__daylabel",
2411
+ "data-today": isToday(day) || void 0,
2412
+ children: [
2413
+ (dayNames[language] ?? dayNames.en ?? [])[day.getDay()],
2414
+ " (",
2415
+ day.getDate(),
2416
+ ")"
2417
+ ]
2418
+ }
2419
+ )
2420
+ },
2421
+ day.toISOString()
2422
+ );
2423
+ }),
2424
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__hours", children: HOURS2.map((hour) => /* @__PURE__ */ jsx("div", { className: "sft-cal-timegrid__hour", children: /* @__PURE__ */ jsxs("span", { className: "sft-cal-timegrid__hourlabel", children: [
2425
+ hour.toString().padStart(2, "0"),
2426
+ ":00"
2427
+ ] }) }, hour)) }),
2428
+ weekDays.map((day, dayIndex) => /* @__PURE__ */ jsxs(
2429
+ "div",
2430
+ {
2431
+ className: "sft-cal-timegrid__col",
2432
+ ref: (el) => {
2433
+ colRefs.current[dayIndex] = el;
2434
+ },
2435
+ onPointerDown: (e) => beginCreate(e, dayIndex),
2436
+ children: [
2437
+ HOURS2.map((hour) => /* @__PURE__ */ jsx(
2438
+ "button",
2439
+ {
2440
+ ref: (el) => {
2441
+ slotRefs.current[`${dayIndex}-${hour}`] = el;
2442
+ },
2443
+ type: "button",
2444
+ className: "sft-cal-timegrid__slot",
2445
+ tabIndex: activeCell.dayIndex === dayIndex && activeCell.hour === hour ? 0 : -1,
2446
+ "aria-label": `${(dayNames[language] ?? dayNames.en ?? [])[day.getDay()]} ${day.getDate()}, ${hour.toString().padStart(2, "0")}:00`,
2447
+ onFocus: () => setFocusedCell({ dayIndex, hour }),
2448
+ onKeyDown: (e) => {
2449
+ let nd = dayIndex;
2450
+ let nh = hour;
2451
+ if (e.key === "ArrowDown") nh = Math.min(23, hour + 1);
2452
+ else if (e.key === "ArrowUp") nh = Math.max(0, hour - 1);
2453
+ else if (e.key === "ArrowRight") nd = Math.min(weekDays.length - 1, dayIndex + 1);
2454
+ else if (e.key === "ArrowLeft") nd = Math.max(0, dayIndex - 1);
2455
+ else return;
2456
+ e.preventDefault();
2457
+ setFocusedCell({ dayIndex: nd, hour: nh });
2458
+ },
2459
+ onClick: (e) => {
2460
+ if (e.target.closest('[data-event="true"]')) return;
2461
+ onAddEventClick(day, hour, e.clientX, e.clientY);
2462
+ }
2463
+ },
2464
+ hour
2465
+ )),
2466
+ eventsForDay(day).map((event) => {
2467
+ if (moveRef.current?.event.id === event.id && moveRef.current.mode === "move" && moveDayIndex != null && moveDayIndex !== dayIndex) {
2468
+ return null;
2469
+ }
2470
+ const isEditing = selectedEvent?.id === event.id && previewEvent;
2471
+ const live = livePreview?.id === event.id ? livePreview : null;
2472
+ const data = isEditing ? {
2473
+ title: previewEvent.title,
2474
+ startTime: previewEvent.startTime,
2475
+ endTime: previewEvent.endTime,
2476
+ categoryId: previewEvent.categoryId
2477
+ } : event;
2478
+ const color = categories.find((c) => c.id === data.categoryId)?.color || "#E30000";
2479
+ const startMin = live ? live.startMin : hhmmToMinutes(data.startTime);
2480
+ const endMin = live ? live.endMin : data.endTime ? hhmmToMinutes(data.endTime) : startMin + 60;
2481
+ const top = startMin / (24 * 60) * 100;
2482
+ const height = (endMin - startMin) / (24 * 60) * 100;
2483
+ const resizable = !event.recurrence && !event.isRecurringInstance;
2484
+ return /* @__PURE__ */ jsxs(
2485
+ "div",
2486
+ {
2487
+ "data-event": "true",
2488
+ "data-movable": "true",
2489
+ className: "sft-cal-timeevent",
2490
+ "data-editing": isEditing || void 0,
2491
+ role: "button",
2492
+ tabIndex: 0,
2493
+ "aria-label": data.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)"),
2494
+ style: {
2495
+ top: `${top}%`,
2496
+ height: `${height}%`,
2497
+ left: "4px",
2498
+ width: "calc(100% - 8px)",
2499
+ background: withAlpha(color, 0.12),
2500
+ borderLeft: `3px solid ${color}`
2501
+ },
2502
+ onPointerDown: (e) => beginMove(e, event, dayIndex, "move"),
2503
+ onClick: (e) => handleEventItemClick(e, event),
2504
+ onKeyDown: (e) => {
2505
+ if (e.key === "Enter" || e.key === " ") {
2506
+ e.preventDefault();
2507
+ e.stopPropagation();
2508
+ onEventClick(event, e.currentTarget);
2509
+ }
2510
+ },
2511
+ children: [
2512
+ resizable && /* @__PURE__ */ jsx(
2513
+ "span",
2514
+ {
2515
+ className: "sft-cal-timeevent__resize sft-cal-timeevent__resize--top",
2516
+ onPointerDown: (e) => beginMove(e, event, dayIndex, "resize-start")
2517
+ }
2518
+ ),
2519
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-timeevent__label", style: { color }, children: [
2520
+ event.recurrence && /* @__PURE__ */ jsx(Repeat, { size: 12 }),
2521
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-timeevent__title", children: data.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)") })
2522
+ ] }),
2523
+ resizable && /* @__PURE__ */ jsx(
2524
+ "span",
2525
+ {
2526
+ className: "sft-cal-timeevent__resize sft-cal-timeevent__resize--bottom",
2527
+ onPointerDown: (e) => beginMove(e, event, dayIndex, "resize-end")
2528
+ }
2529
+ )
2530
+ ]
2531
+ },
2532
+ event.id
2533
+ );
2534
+ }),
2535
+ createPreview && createPreview.dayIndex === dayIndex && (() => {
2536
+ const s = Math.min(createPreview.startMin, createPreview.endMin);
2537
+ const e = Math.max(createPreview.startMin, createPreview.endMin);
2538
+ const top = s / (24 * 60) * 100;
2539
+ const height = (e - s) / (24 * 60) * 100;
2540
+ return /* @__PURE__ */ jsx(
2541
+ "div",
2542
+ {
2543
+ className: "sft-cal-timeevent sft-cal-timeevent--draft",
2544
+ style: {
2545
+ top: `${top}%`,
2546
+ height: `${height}%`,
2547
+ left: "4px",
2548
+ width: "calc(100% - 8px)"
2549
+ },
2550
+ children: /* @__PURE__ */ jsx("div", { className: "sft-cal-timeevent__label", children: /* @__PURE__ */ jsxs("span", { className: "sft-cal-timeevent__title", children: [
2551
+ minutesToHHMM(s),
2552
+ "\u2013",
2553
+ minutesToHHMM(e)
2554
+ ] }) })
2555
+ }
2556
+ );
2557
+ })(),
2558
+ moveDayIndex === dayIndex && livePreview && (() => {
2559
+ const mv = moveRef.current;
2560
+ if (!mv || mv.mode !== "move") return null;
2561
+ const originalDayIndex = weekDays.findIndex((d) => isSameDay(d, mv.event.date));
2562
+ if (originalDayIndex === dayIndex) return null;
2563
+ const color = categories.find((c) => c.id === mv.event.categoryId)?.color || "#E30000";
2564
+ const top = livePreview.startMin / (24 * 60) * 100;
2565
+ const height = (livePreview.endMin - livePreview.startMin) / (24 * 60) * 100;
2566
+ return /* @__PURE__ */ jsx(
2567
+ "div",
2568
+ {
2569
+ className: "sft-cal-timeevent",
2570
+ style: {
2571
+ top: `${top}%`,
2572
+ height: `${height}%`,
2573
+ left: "4px",
2574
+ width: "calc(100% - 8px)",
2575
+ background: withAlpha(color, 0.12),
2576
+ borderLeft: `3px solid ${color}`,
2577
+ pointerEvents: "none"
2578
+ },
2579
+ children: /* @__PURE__ */ jsxs("div", { className: "sft-cal-timeevent__label", style: { color }, children: [
2580
+ mv.event.recurrence && /* @__PURE__ */ jsx(Repeat, { size: 12 }),
2581
+ /* @__PURE__ */ jsx("span", { className: "sft-cal-timeevent__title", children: mv.event.title || pick(language, "(\uC81C\uBAA9 \uC5C6\uC74C)", "(No title)") })
2582
+ ] })
2583
+ }
2584
+ );
2585
+ })()
2586
+ ]
2587
+ },
2588
+ `col-${day.toISOString()}`
2589
+ ))
2590
+ ] });
2591
+ }
2592
+ var MINI_DAY_LABELS = {
2593
+ ko: ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"],
2594
+ en: ["S", "M", "T", "W", "T", "F", "S"]
2595
+ };
2596
+ function YearView({
2597
+ currentDate,
2598
+ events,
2599
+ selectedCategoryIds,
2600
+ categories,
2601
+ language,
2602
+ onDateClick,
2603
+ onViewChange
2604
+ }) {
2605
+ const year = currentDate.getFullYear();
2606
+ const months = Array.from({ length: 12 }, (_, i) => i);
2607
+ const containerRef = useRef(null);
2608
+ const handleDayKeyDown = (e, date) => {
2609
+ let delta = 0;
2610
+ if (e.key === "ArrowRight") delta = 1;
2611
+ else if (e.key === "ArrowLeft") delta = -1;
2612
+ else if (e.key === "ArrowDown") delta = 7;
2613
+ else if (e.key === "ArrowUp") delta = -7;
2614
+ else return;
2615
+ e.preventDefault();
2616
+ const next = new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta);
2617
+ if (next.getFullYear() !== year) return;
2618
+ const target = containerRef.current?.querySelector(
2619
+ `[data-daykey="${formatDate(next)}"]`
2620
+ );
2621
+ target?.focus();
2622
+ };
2623
+ const expanded = expandRecurringEvents(
2624
+ events,
2625
+ new Date(year, 0, 1),
2626
+ new Date(year, 11, 31)
2627
+ ).filter((e) => !e.categoryId || selectedCategoryIds.includes(e.categoryId));
2628
+ const countForMonth = (monthIndex) => {
2629
+ const start = new Date(year, monthIndex, 1);
2630
+ const end = new Date(year, monthIndex + 1, 0);
2631
+ return expanded.filter((e) => {
2632
+ const s = e.date;
2633
+ const en = e.endDate ?? e.date;
2634
+ return s >= start && s <= end || en >= start && en <= end || s < start && en > end;
2635
+ }).length;
2636
+ };
2637
+ const renderMini = (monthIndex) => {
2638
+ const firstDay = new Date(year, monthIndex, 1).getDay();
2639
+ const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
2640
+ const cells = [
2641
+ ...Array(firstDay).fill(null),
2642
+ ...Array.from({ length: daysInMonth }, (_, i) => i + 1)
2643
+ ];
2644
+ const labels = MINI_DAY_LABELS[language] ?? MINI_DAY_LABELS.en ?? [];
2645
+ return /* @__PURE__ */ jsxs("div", { className: "sft-cal-mini", children: [
2646
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal-mini__head", children: [
2647
+ /* @__PURE__ */ jsx("h3", { className: "sft-cal-mini__title", children: pick(
2648
+ language,
2649
+ `${monthIndex + 1}\uC6D4`,
2650
+ new Date(year, monthIndex).toLocaleDateString("en-US", { month: "long" })
2651
+ ) }),
2652
+ /* @__PURE__ */ jsxs("span", { className: "sft-cal-mini__count", children: [
2653
+ countForMonth(monthIndex),
2654
+ " ",
2655
+ pick(language, "\uAC1C \uC77C\uC815", "events")
2656
+ ] })
2657
+ ] }),
2658
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-mini__weekdays", children: labels.map((d, i) => (
2659
+ // biome-ignore lint/suspicious/noArrayIndexKey: fixed 7-item header
2660
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-mini__weekday", children: d }, i)
2661
+ )) }),
2662
+ /* @__PURE__ */ jsx("div", { className: "sft-cal-mini__grid", children: cells.map((day, index) => {
2663
+ const date = day ? new Date(year, monthIndex, day) : null;
2664
+ const today = date ? isToday(date) : false;
2665
+ const dayEvents = date ? expanded.filter((e) => {
2666
+ const norm = new Date(
2667
+ date.getFullYear(),
2668
+ date.getMonth(),
2669
+ date.getDate()
2670
+ ).getTime();
2671
+ const s = new Date(
2672
+ e.date.getFullYear(),
2673
+ e.date.getMonth(),
2674
+ e.date.getDate()
2675
+ ).getTime();
2676
+ const en = e.endDate ? new Date(
2677
+ e.endDate.getFullYear(),
2678
+ e.endDate.getMonth(),
2679
+ e.endDate.getDate()
2680
+ ).getTime() : s;
2681
+ return norm >= s && norm <= en;
2682
+ }) : [];
2683
+ return /* @__PURE__ */ jsxs(
2684
+ "button",
2685
+ {
2686
+ type: "button",
2687
+ className: "sft-cal-mini__day",
2688
+ "data-today": today || void 0,
2689
+ "data-empty": !day || void 0,
2690
+ "data-daykey": date ? formatDate(date) : void 0,
2691
+ disabled: !day,
2692
+ "aria-label": date ? `${year}-${monthIndex + 1}-${day}` : void 0,
2693
+ onKeyDown: (e) => date && handleDayKeyDown(e, date),
2694
+ onClick: () => {
2695
+ if (day) {
2696
+ onDateClick(new Date(year, monthIndex, 1));
2697
+ onViewChange("month");
2698
+ }
2699
+ },
2700
+ children: [
2701
+ day,
2702
+ dayEvents.length > 0 && !today && /* @__PURE__ */ jsx("span", { className: "sft-cal-mini__dots", children: dayEvents.slice(0, 3).map((e, i) => /* @__PURE__ */ jsx(
2703
+ "span",
2704
+ {
2705
+ className: "sft-cal-mini__dot",
2706
+ style: {
2707
+ background: categories.find((c) => c.id === e.categoryId)?.color || "#E30000"
2708
+ }
2709
+ },
2710
+ i
2711
+ )) })
2712
+ ]
2713
+ },
2714
+ day ? `d${day}` : `empty-${index}`
2715
+ );
2716
+ }) })
2717
+ ] });
2718
+ };
2719
+ return /* @__PURE__ */ jsx("div", { className: "sft-cal-year", ref: containerRef, children: /* @__PURE__ */ jsx("div", { className: "sft-cal-year__grid", children: months.map((m) => /* @__PURE__ */ jsx("div", { className: "sft-cal-year__cell", children: renderMini(m) }, m)) }) });
2720
+ }
2721
+ var startOfDay3 = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
2722
+ function Calendar({
2723
+ language = "ko",
2724
+ controller,
2725
+ initialEvents,
2726
+ initialCategories,
2727
+ initialDate,
2728
+ initialView = "month",
2729
+ holidays = [],
2730
+ className
2731
+ }) {
2732
+ const internal = useCalendar({ initialEvents, initialCategories });
2733
+ const ctrl = controller ?? internal;
2734
+ const {
2735
+ events,
2736
+ addEvent,
2737
+ updateEvent,
2738
+ deleteEvent,
2739
+ setEvents,
2740
+ canUndo,
2741
+ canRedo,
2742
+ undo,
2743
+ redo,
2744
+ categories,
2745
+ selectedCategoryIds,
2746
+ toggleCategory,
2747
+ addCategory,
2748
+ updateCategory,
2749
+ deleteCategory,
2750
+ reorderCategories
2751
+ } = ctrl;
2752
+ const [currentDate, setCurrentDate] = useState(initialDate ?? /* @__PURE__ */ new Date());
2753
+ const [viewType, setViewType] = useState(initialView);
2754
+ const [modalOpen, setModalOpen] = useState(false);
2755
+ const [selectedEvent, setSelectedEvent] = useState(null);
2756
+ const [selectedDate, setSelectedDate] = useState(null);
2757
+ const [selectedEndDate, setSelectedEndDate] = useState(null);
2758
+ const [previewEvent, setPreviewEvent] = useState(null);
2759
+ const [expandedRows, setExpandedRows] = useState(/* @__PURE__ */ new Set());
2760
+ const [showSearch, setShowSearch] = useState(false);
2761
+ const [searchQuery, setSearchQuery] = useState("");
2762
+ const [defaultStart, setDefaultStart] = useState("09:00");
2763
+ const [defaultEnd, setDefaultEnd] = useState("10:00");
2764
+ const instanceDateRef = useRef(null);
2765
+ const searchRef = useRef(null);
2766
+ const [pendingMove, setPendingMove] = useState(null);
2767
+ const [moveScope, setMoveScope] = useState("this");
2768
+ const filteredEvents = useMemo(() => {
2769
+ const q = searchQuery.trim().toLowerCase();
2770
+ if (!q) return events;
2771
+ return events.filter(
2772
+ (e) => e.title.toLowerCase().includes(q) || (e.description ?? "").toLowerCase().includes(q)
2773
+ );
2774
+ }, [events, searchQuery]);
2775
+ const previousPeriod = () => setCurrentDate(getPreviousPeriodDate(currentDate, viewType));
2776
+ const nextPeriod = () => setCurrentDate(getNextPeriodDate(currentDate, viewType));
2777
+ const goToToday = () => setCurrentDate(/* @__PURE__ */ new Date());
2778
+ const headerTitle = useMemo(() => {
2779
+ const y = currentDate.getFullYear();
2780
+ const mn = monthNames[language] ?? monthNames.en;
2781
+ const mon = (d) => mn[d.getMonth()] ?? "";
2782
+ if (viewType === "year") return `${y}`;
2783
+ if (viewType === "day") {
2784
+ return pick(
2785
+ language,
2786
+ `${y}\uB144 ${mon(currentDate)} ${currentDate.getDate()}\uC77C`,
2787
+ `${mon(currentDate)} ${currentDate.getDate()}, ${y}`
2788
+ );
2789
+ }
2790
+ if (viewType === "week") {
2791
+ const wd = getWeekDays(currentDate);
2792
+ const s = wd[0] ?? currentDate;
2793
+ const e = wd[6] ?? currentDate;
2794
+ return pick(
2795
+ language,
2796
+ `${mon(s)} ${s.getDate()}\uC77C - ${mon(e)} ${e.getDate()}\uC77C`,
2797
+ `${mon(s)} ${s.getDate()} - ${mon(e)} ${e.getDate()}, ${y}`
2798
+ );
2799
+ }
2800
+ return pick(language, `${y}\uB144 ${mon(currentDate)}`, `${mon(currentDate)} ${y}`);
2801
+ }, [currentDate, viewType, language]);
2802
+ useEffect(() => {
2803
+ function onKeyDown(e) {
2804
+ const target = e.target;
2805
+ const typing = !!target && /^(input|textarea)$/i.test(target.tagName);
2806
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
2807
+ if (typing) return;
2808
+ e.preventDefault();
2809
+ if (e.shiftKey) redo();
2810
+ else undo();
2811
+ return;
2812
+ }
2813
+ if (typing || e.metaKey || e.ctrlKey || e.altKey || modalOpen) return;
2814
+ switch (e.key.toLowerCase()) {
2815
+ case "t":
2816
+ goToToday();
2817
+ break;
2818
+ case "m":
2819
+ setViewType("month");
2820
+ break;
2821
+ case "w":
2822
+ setViewType("week");
2823
+ break;
2824
+ case "d":
2825
+ setViewType("day");
2826
+ break;
2827
+ case "y":
2828
+ setViewType("year");
2829
+ break;
2830
+ default:
2831
+ return;
2832
+ }
2833
+ e.preventDefault();
2834
+ }
2835
+ window.addEventListener("keydown", onKeyDown);
2836
+ return () => window.removeEventListener("keydown", onKeyDown);
2837
+ }, [undo, redo, modalOpen, goToToday, setViewType]);
2838
+ const handleEventMove = (event, newDate) => {
2839
+ const deltaDays = Math.round(
2840
+ (startOfDay3(newDate).getTime() - startOfDay3(event.date).getTime()) / 864e5
2841
+ );
2842
+ if (deltaDays === 0) return;
2843
+ const shift = (d) => new Date(
2844
+ d.getFullYear(),
2845
+ d.getMonth(),
2846
+ d.getDate() + deltaDays,
2847
+ d.getHours(),
2848
+ d.getMinutes()
2849
+ );
2850
+ if (event.recurrence || event.isRecurringInstance) {
2851
+ const master = event.isRecurringInstance ? events.find((e) => e.id === event.recurringEventId) ?? event : event;
2852
+ setMoveScope("this");
2853
+ setPendingMove({ master, instanceDate: event.date, newDate: shift(event.date) });
2854
+ return;
2855
+ }
2856
+ updateEvent(event.id, {
2857
+ date: shift(event.date),
2858
+ endDate: event.endDate ? shift(event.endDate) : void 0
2859
+ });
2860
+ };
2861
+ const handleEventResize = (event, edge, newDate) => {
2862
+ const onDay = (base, day) => new Date(
2863
+ day.getFullYear(),
2864
+ day.getMonth(),
2865
+ day.getDate(),
2866
+ base.getHours(),
2867
+ base.getMinutes()
2868
+ );
2869
+ if (edge === "start") {
2870
+ const end = event.endDate ?? event.date;
2871
+ if (startOfDay3(newDate) > startOfDay3(end)) return;
2872
+ updateEvent(event.id, { date: onDay(event.date, newDate) });
2873
+ } else {
2874
+ if (startOfDay3(newDate) < startOfDay3(event.date)) return;
2875
+ updateEvent(event.id, { endDate: onDay(event.endDate ?? event.date, newDate) });
2876
+ }
2877
+ };
2878
+ const openCreate = (date, endDate, hour) => {
2879
+ setSelectedEvent(null);
2880
+ instanceDateRef.current = null;
2881
+ setSelectedDate(date);
2882
+ setSelectedEndDate(endDate);
2883
+ if (hour != null) {
2884
+ setDefaultStart(`${String(hour).padStart(2, "0")}:00`);
2885
+ setDefaultEnd(`${String(Math.min(23, hour + 1)).padStart(2, "0")}:00`);
2886
+ } else {
2887
+ const s = getNextAvailableTime(date, events);
2888
+ const sh = Number.parseInt(s.split(":")[0] ?? "9", 10);
2889
+ setDefaultStart(s);
2890
+ setDefaultEnd(`${String(Math.min(23, sh + 1)).padStart(2, "0")}:00`);
2891
+ }
2892
+ setModalOpen(true);
2893
+ };
2894
+ const handleEventClick = (event) => {
2895
+ const master = event.isRecurringInstance ? events.find((e) => e.id === event.recurringEventId) ?? event : event;
2896
+ instanceDateRef.current = event.date;
2897
+ setSelectedEvent(master);
2898
+ setSelectedDate(master.date);
2899
+ setSelectedEndDate(master.endDate ?? null);
2900
+ setModalOpen(true);
2901
+ };
2902
+ const handleAddEventClick = (date, hour) => openCreate(date, null, hour);
2903
+ const handleRangeSelect = (start, end) => openCreate(start, end);
2904
+ const handleTimeRangeSelect = (date, startMin, endMin) => {
2905
+ setSelectedEvent(null);
2906
+ instanceDateRef.current = null;
2907
+ setSelectedDate(date);
2908
+ setSelectedEndDate(null);
2909
+ setDefaultStart(minutesToHHMM(startMin));
2910
+ setDefaultEnd(minutesToHHMM(endMin));
2911
+ setModalOpen(true);
2912
+ };
2913
+ const handleEventTimeChange = (event, startMin, endMin, newDate) => {
2914
+ if (event.recurrence || event.isRecurringInstance) {
2915
+ const master = event.isRecurringInstance ? events.find((e) => e.id === event.recurringEventId) ?? event : event;
2916
+ const resolvedDate = newDate ? new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate()) : startOfDay3(event.date);
2917
+ setMoveScope("this");
2918
+ setPendingMove({
2919
+ master,
2920
+ instanceDate: event.date,
2921
+ newDate: resolvedDate,
2922
+ newStartTime: minutesToHHMM(startMin),
2923
+ newEndTime: minutesToHHMM(endMin)
2924
+ });
2925
+ return;
2926
+ }
2927
+ const patch = {
2928
+ startTime: minutesToHHMM(startMin),
2929
+ endTime: minutesToHHMM(endMin)
2930
+ };
2931
+ if (newDate)
2932
+ patch.date = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
2933
+ updateEvent(event.id, patch);
2934
+ };
2935
+ const handleSave = (data) => {
2936
+ const base = {
2937
+ title: data.title,
2938
+ date: data.startDate,
2939
+ endDate: data.endDate,
2940
+ startTime: data.startTime,
2941
+ endTime: data.endTime,
2942
+ description: data.description,
2943
+ categoryId: data.categoryId,
2944
+ recurrence: data.recurrence
2945
+ };
2946
+ if (selectedEvent) updateEvent(selectedEvent.id, base);
2947
+ else addEvent(base);
2948
+ setPreviewEvent(null);
2949
+ };
2950
+ const handleDelete = (deleteType) => {
2951
+ if (!selectedEvent) return;
2952
+ const master = selectedEvent;
2953
+ const instanceDate = instanceDateRef.current;
2954
+ if (!master.recurrence || deleteType === "all") {
2955
+ deleteEvent(master.id);
2956
+ } else if (deleteType === "this" && instanceDate) {
2957
+ setEvents(
2958
+ (prev) => prev.map(
2959
+ (e) => e.id === master.id ? { ...e, exdate: [...e.exdate ?? [], instanceDate] } : e
2960
+ )
2961
+ );
2962
+ } else if (deleteType === "following" && instanceDate) {
2963
+ const until = startOfDay3(instanceDate);
2964
+ until.setDate(until.getDate() - 1);
2965
+ setEvents(
2966
+ (prev) => prev.map(
2967
+ (e) => e.id === master.id && e.recurrence ? { ...e, recurrence: { ...e.recurrence, until } } : e
2968
+ )
2969
+ );
2970
+ }
2971
+ setPreviewEvent(null);
2972
+ };
2973
+ const handleRecurrenceScopeConfirm = () => {
2974
+ if (!pendingMove) return;
2975
+ const { master, instanceDate, newDate, newStartTime, newEndTime } = pendingMove;
2976
+ if (moveScope === "all" || !master.recurrence) {
2977
+ const deltaDays = Math.round(
2978
+ (startOfDay3(newDate).getTime() - startOfDay3(instanceDate).getTime()) / 864e5
2979
+ );
2980
+ const shift = (d) => new Date(
2981
+ d.getFullYear(),
2982
+ d.getMonth(),
2983
+ d.getDate() + deltaDays,
2984
+ d.getHours(),
2985
+ d.getMinutes()
2986
+ );
2987
+ updateEvent(master.id, {
2988
+ date: shift(master.date),
2989
+ endDate: master.endDate ? shift(master.endDate) : void 0,
2990
+ startTime: newStartTime ?? master.startTime,
2991
+ endTime: newEndTime ?? master.endTime
2992
+ });
2993
+ } else if (moveScope === "this") {
2994
+ setEvents((prev) => [
2995
+ ...prev.map(
2996
+ (e) => e.id === master.id ? { ...e, exdate: [...e.exdate ?? [], instanceDate] } : e
2997
+ ),
2998
+ {
2999
+ id: `${master.id}-moved-${instanceDate.getTime()}`,
3000
+ title: master.title,
3001
+ date: newDate,
3002
+ startTime: newStartTime ?? master.startTime,
3003
+ endTime: newEndTime ?? master.endTime,
3004
+ description: master.description,
3005
+ categoryId: master.categoryId
3006
+ }
3007
+ ]);
3008
+ } else if (moveScope === "following" && master.recurrence) {
3009
+ const recurrence = master.recurrence;
3010
+ const until = startOfDay3(instanceDate);
3011
+ until.setDate(until.getDate() - 1);
3012
+ const elapsed = occurrencesInRange(master.date, recurrence, master.date, until).length;
3013
+ const remainingCount = recurrence.count != null ? Math.max(1, recurrence.count - elapsed) : void 0;
3014
+ setEvents((prev) => [
3015
+ ...prev.map(
3016
+ (e) => e.id === master.id && e.recurrence ? { ...e, recurrence: { ...e.recurrence, until } } : e
3017
+ ),
3018
+ {
3019
+ id: `${master.id}-split-${instanceDate.getTime()}`,
3020
+ title: master.title,
3021
+ date: newDate,
3022
+ startTime: newStartTime ?? master.startTime,
3023
+ endTime: newEndTime ?? master.endTime,
3024
+ description: master.description,
3025
+ categoryId: master.categoryId,
3026
+ recurrence: { ...recurrence, count: remainingCount }
3027
+ }
3028
+ ]);
3029
+ }
3030
+ setPendingMove(null);
3031
+ };
3032
+ const handleRecurrenceScopeCancel = () => setPendingMove(null);
3033
+ const viewOptions = [
3034
+ { value: "day", label: pick(language, "\uC77C\uAC04", "Day") },
3035
+ { value: "week", label: pick(language, "\uC8FC\uAC04", "Week") },
3036
+ { value: "month", label: pick(language, "\uC6D4\uAC04", "Month") },
3037
+ { value: "year", label: pick(language, "\uC5F0\uAC04", "Year") }
3038
+ ];
3039
+ return /* @__PURE__ */ jsxs("div", { className: className ? `sft-cal ${className}` : "sft-cal", children: [
3040
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal__toolbar", children: [
3041
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal__toolbar-left", children: [
3042
+ /* @__PURE__ */ jsx("h2", { className: "sft-cal__title", children: headerTitle }),
3043
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal__nav", children: [
3044
+ /* @__PURE__ */ jsx(
3045
+ "button",
3046
+ {
3047
+ type: "button",
3048
+ className: "sft-cal__navbtn",
3049
+ "aria-label": "previous",
3050
+ onClick: previousPeriod,
3051
+ children: /* @__PURE__ */ jsx(ChevronLeft, { size: 16 })
3052
+ }
3053
+ ),
3054
+ /* @__PURE__ */ jsx(
3055
+ "button",
3056
+ {
3057
+ type: "button",
3058
+ className: "sft-cal__navbtn",
3059
+ "aria-label": "next",
3060
+ onClick: nextPeriod,
3061
+ children: /* @__PURE__ */ jsx(ChevronRight, { size: 16 })
3062
+ }
3063
+ )
3064
+ ] }),
3065
+ /* @__PURE__ */ jsx(
3066
+ "button",
3067
+ {
3068
+ type: "button",
3069
+ className: "sft-cal-btn sft-cal-btn--outline sft-cal-btn--sm",
3070
+ onClick: goToToday,
3071
+ children: pick(language, "\uC624\uB298", "Today")
3072
+ }
3073
+ ),
3074
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal__nav", children: [
3075
+ /* @__PURE__ */ jsx(
3076
+ "button",
3077
+ {
3078
+ type: "button",
3079
+ className: "sft-cal__navbtn",
3080
+ "aria-label": pick(language, "\uC2E4\uD589 \uCDE8\uC18C", "Undo"),
3081
+ title: pick(language, "\uC2E4\uD589 \uCDE8\uC18C (\u2318Z)", "Undo (\u2318Z)"),
3082
+ disabled: !canUndo,
3083
+ onClick: undo,
3084
+ children: /* @__PURE__ */ jsx(Undo2, { size: 16 })
3085
+ }
3086
+ ),
3087
+ /* @__PURE__ */ jsx(
3088
+ "button",
3089
+ {
3090
+ type: "button",
3091
+ className: "sft-cal__navbtn",
3092
+ "aria-label": pick(language, "\uB2E4\uC2DC \uC2E4\uD589", "Redo"),
3093
+ title: pick(language, "\uB2E4\uC2DC \uC2E4\uD589 (\u2318\u21E7Z)", "Redo (\u2318\u21E7Z)"),
3094
+ disabled: !canRedo,
3095
+ onClick: redo,
3096
+ children: /* @__PURE__ */ jsx(Redo2, { size: 16 })
3097
+ }
3098
+ )
3099
+ ] }),
3100
+ /* @__PURE__ */ jsx("div", { className: "sft-cal__search", children: showSearch ? /* @__PURE__ */ jsxs("div", { className: "sft-cal__searchbox", children: [
3101
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "sft-cal__searchicon" }),
3102
+ /* @__PURE__ */ jsx(
3103
+ "input",
3104
+ {
3105
+ ref: searchRef,
3106
+ className: "sft-cal__searchinput",
3107
+ placeholder: pick(language, "\uC77C\uC815 \uAC80\uC0C9...", "Search events..."),
3108
+ value: searchQuery,
3109
+ onChange: (e) => setSearchQuery(e.target.value),
3110
+ onKeyDown: (e) => {
3111
+ if (e.key === "Escape") {
3112
+ setSearchQuery("");
3113
+ setShowSearch(false);
3114
+ }
3115
+ }
3116
+ }
3117
+ ),
3118
+ /* @__PURE__ */ jsx(
3119
+ "button",
3120
+ {
3121
+ type: "button",
3122
+ className: "sft-cal__searchclose",
3123
+ "aria-label": "close search",
3124
+ onClick: () => {
3125
+ setSearchQuery("");
3126
+ setShowSearch(false);
3127
+ },
3128
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
3129
+ }
3130
+ )
3131
+ ] }) : /* @__PURE__ */ jsx(
3132
+ "button",
3133
+ {
3134
+ type: "button",
3135
+ className: "sft-cal__navbtn",
3136
+ "aria-label": "search",
3137
+ onClick: () => {
3138
+ setShowSearch(true);
3139
+ setTimeout(() => searchRef.current?.focus(), 30);
3140
+ },
3141
+ children: /* @__PURE__ */ jsx(Search, { size: 16 })
3142
+ }
3143
+ ) })
3144
+ ] }),
3145
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal__toolbar-right", children: [
3146
+ /* @__PURE__ */ jsx(
3147
+ CategoryFilter,
3148
+ {
3149
+ categories,
3150
+ selectedCategoryIds,
3151
+ language,
3152
+ onToggle: toggleCategory,
3153
+ onAdd: addCategory,
3154
+ onUpdate: updateCategory,
3155
+ onDelete: deleteCategory,
3156
+ onReorder: reorderCategories
3157
+ }
3158
+ ),
3159
+ /* @__PURE__ */ jsx(SegmentTabs, { value: viewType, onValueChange: setViewType, options: viewOptions })
3160
+ ] })
3161
+ ] }),
3162
+ /* @__PURE__ */ jsxs("div", { className: "sft-cal__view", children: [
3163
+ viewType === "month" && /* @__PURE__ */ jsx(
3164
+ MonthView,
3165
+ {
3166
+ currentDate,
3167
+ events: filteredEvents,
3168
+ selectedCategoryIds,
3169
+ categories,
3170
+ language,
3171
+ holidays,
3172
+ selectedEvent,
3173
+ previewEvent: modalOpen ? previewEvent : null,
3174
+ expandedRows,
3175
+ setExpandedRows,
3176
+ onEventClick: handleEventClick,
3177
+ onAddEventClick: handleAddEventClick,
3178
+ onRangeSelect: handleRangeSelect,
3179
+ onEventMove: handleEventMove,
3180
+ onEventResize: handleEventResize
3181
+ }
3182
+ ),
3183
+ viewType === "week" && /* @__PURE__ */ jsx(
3184
+ WeekView,
3185
+ {
3186
+ currentDate,
3187
+ events: filteredEvents,
3188
+ selectedCategoryIds,
3189
+ categories,
3190
+ language,
3191
+ selectedEvent,
3192
+ previewEvent: modalOpen ? previewEvent : null,
3193
+ onEventClick: handleEventClick,
3194
+ onAddEventClick: handleAddEventClick,
3195
+ onTimeRangeSelect: handleTimeRangeSelect,
3196
+ onEventTimeChange: handleEventTimeChange
3197
+ }
3198
+ ),
3199
+ viewType === "day" && /* @__PURE__ */ jsx(
3200
+ DayView,
3201
+ {
3202
+ currentDate,
3203
+ events: filteredEvents,
3204
+ selectedCategoryIds,
3205
+ categories,
3206
+ language,
3207
+ selectedEvent,
3208
+ previewEvent: modalOpen ? previewEvent : null,
3209
+ onEventClick: handleEventClick,
3210
+ onAddEventClick: handleAddEventClick,
3211
+ onTimeRangeSelect: handleTimeRangeSelect,
3212
+ onEventTimeChange: handleEventTimeChange
3213
+ }
3214
+ ),
3215
+ viewType === "year" && /* @__PURE__ */ jsx(
3216
+ YearView,
3217
+ {
3218
+ currentDate,
3219
+ events: filteredEvents,
3220
+ selectedCategoryIds,
3221
+ categories,
3222
+ language,
3223
+ onDateClick: setCurrentDate,
3224
+ onViewChange: setViewType
3225
+ }
3226
+ )
3227
+ ] }),
3228
+ /* @__PURE__ */ jsx(
3229
+ EventModal,
3230
+ {
3231
+ open: modalOpen,
3232
+ onOpenChange: (o) => {
3233
+ setModalOpen(o);
3234
+ if (!o) {
3235
+ setSelectedEvent(null);
3236
+ setPreviewEvent(null);
3237
+ }
3238
+ },
3239
+ event: selectedEvent,
3240
+ selectedDate,
3241
+ selectedEndDate,
3242
+ defaultStartTime: defaultStart,
3243
+ defaultEndTime: defaultEnd,
3244
+ categories,
3245
+ selectedCategoryIds,
3246
+ language,
3247
+ onSave: handleSave,
3248
+ onDelete: selectedEvent ? handleDelete : void 0,
3249
+ onChange: setPreviewEvent,
3250
+ onToggleCategory: toggleCategory,
3251
+ onAddCategory: addCategory,
3252
+ onUpdateCategory: updateCategory,
3253
+ onDeleteCategory: deleteCategory,
3254
+ onReorderCategories: reorderCategories
3255
+ }
3256
+ ),
3257
+ pendingMove && /* @__PURE__ */ jsx(
3258
+ RecurrenceScopeDialog,
3259
+ {
3260
+ language,
3261
+ selectedScope: moveScope,
3262
+ setSelectedScope: setMoveScope,
3263
+ onCancel: handleRecurrenceScopeCancel,
3264
+ onConfirm: handleRecurrenceScopeConfirm
3265
+ }
3266
+ )
3267
+ ] });
3268
+ }
3269
+
3270
+ // src/index.ts
3271
+ var VERSION = "0.0.0";
3272
+
3273
+ export { COLOR_PRESETS, Calendar, CategoryFilter, CategoryItem, DayView, DeleteOptionsDialog, EventModal, MonthView, RecurrenceScopeDialog, RecurrenceSection, SegmentTabs, VERSION, WeekView, YearView, dayNames, eventCoversDate, expandRecurringEvents, findClosestPresetColor, formatDate, getDaysInMonth, getEventsForDate, getNextAvailableTime, getNextPeriodDate, getPreviousPeriodDate, getWeekDays, hhmmToMinutes, isCurrentMonth, isSameDay, isToday, minutesFromY, minutesToHHMM, monthNames, occurrencesInRange, snapMinutes, useCalendar, withAlpha };
3274
+ //# sourceMappingURL=index.js.map
3275
+ //# sourceMappingURL=index.js.map