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