@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.
@@ -0,0 +1,443 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * softium-ui Calendar — core types.
6
+ *
7
+ * Ported from the calendary project into softium-ui's token-CSS conventions.
8
+ * Server / Google-Calendar / today-task concerns are intentionally dropped —
9
+ * this is a pure UI component driven by local state (or host-provided state).
10
+ */
11
+ type ViewType = 'day' | 'week' | 'month' | 'year';
12
+ type RecurrenceFreq = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
13
+ interface Recurrence {
14
+ freq: RecurrenceFreq;
15
+ /** every N periods (1 = every period) */
16
+ interval: number;
17
+ /** weekly only — weekdays as Mon=0 … Sun=6 (matches the recurrence editor) */
18
+ byweekday?: number[];
19
+ /** repeat until this date (inclusive) */
20
+ until?: Date;
21
+ /** repeat this many occurrences */
22
+ count?: number;
23
+ }
24
+ interface Category {
25
+ id: string;
26
+ name: string;
27
+ /** hex color, e.g. "#E30000" */
28
+ color: string;
29
+ }
30
+ /** a single day's holiday/observance label, shown in the month view's date header */
31
+ interface Holiday {
32
+ date: Date;
33
+ name: string;
34
+ }
35
+ interface CalendarEvent {
36
+ id: string;
37
+ title: string;
38
+ /** start date (time-of-day carried separately in startTime) */
39
+ date: Date;
40
+ /** end date for multi-day / period events */
41
+ endDate?: Date;
42
+ /** "HH:MM" */
43
+ startTime?: string;
44
+ /** "HH:MM" */
45
+ endTime?: string;
46
+ description?: string;
47
+ categoryId?: string;
48
+ recurrence?: Recurrence;
49
+ /** dates excluded from a recurring series */
50
+ exdate?: Date[];
51
+ /** true for a generated occurrence of a recurring series (never persisted) */
52
+ isRecurringInstance?: boolean;
53
+ /** id of the master event a recurring instance came from */
54
+ recurringEventId?: string;
55
+ }
56
+ /** the working draft shown in an event's time-grid position while editing */
57
+ interface PreviewEvent {
58
+ title: string;
59
+ startTime: string;
60
+ endTime: string;
61
+ description: string;
62
+ categoryId: string;
63
+ startDate?: Date;
64
+ endDate?: Date;
65
+ isPeriod?: boolean;
66
+ }
67
+
68
+ interface UseCalendarOptions {
69
+ initialEvents?: CalendarEvent[];
70
+ initialCategories?: Category[];
71
+ }
72
+ interface CalendarController {
73
+ events: CalendarEvent[];
74
+ setEvents: React.Dispatch<React.SetStateAction<CalendarEvent[]>>;
75
+ addEvent: (data: Omit<CalendarEvent, 'id'>) => CalendarEvent;
76
+ updateEvent: (id: string, data: Partial<CalendarEvent>) => void;
77
+ deleteEvent: (id: string) => void;
78
+ /** undo/redo over event add/update/delete/move (see setEvents) */
79
+ canUndo: boolean;
80
+ canRedo: boolean;
81
+ undo: () => void;
82
+ redo: () => void;
83
+ categories: Category[];
84
+ setCategories: React.Dispatch<React.SetStateAction<Category[]>>;
85
+ addCategory: (data: {
86
+ name: string;
87
+ color: string;
88
+ }) => string;
89
+ updateCategory: (id: string, data: Partial<Category>) => void;
90
+ deleteCategory: (id: string) => void;
91
+ reorderCategories: (from: number, to: number) => void;
92
+ selectedCategoryIds: string[];
93
+ toggleCategory: (id: string) => void;
94
+ setSelectedCategoryIds: React.Dispatch<React.SetStateAction<string[]>>;
95
+ }
96
+ /**
97
+ * useCalendar — self-contained local state for events + categories + the
98
+ * category filter selection. No server/Google concerns; this is the state a
99
+ * host would otherwise wire to its own backend.
100
+ */
101
+ declare function useCalendar(options?: UseCalendarOptions): CalendarController;
102
+
103
+ interface CalendarProps {
104
+ /** UI language for the calendar's own strings. Default 'ko'. */
105
+ language?: string;
106
+ /** an external controller (from useCalendar); one is created internally if omitted */
107
+ controller?: CalendarController;
108
+ initialEvents?: CalendarEvent[];
109
+ initialCategories?: Category[];
110
+ initialDate?: Date;
111
+ initialView?: ViewType;
112
+ /** holidays/observances shown as a label in the month view's date header */
113
+ holidays?: Holiday[];
114
+ className?: string;
115
+ }
116
+ /**
117
+ * Calendar — the full calendar UI: a toolbar (title, prev/next/today, search,
118
+ * category filter, view switcher) over month/week/day/year views, with an
119
+ * event create/edit modal. Ported from the calendary project into softium
120
+ * token CSS; server / Google-Calendar / today-task concerns removed. Runs off
121
+ * local state (or a host-provided controller).
122
+ */
123
+ declare function Calendar({ language, controller, initialEvents, initialCategories, initialDate, initialView, holidays, className, }: CalendarProps): ReactNode;
124
+
125
+ interface MonthViewProps {
126
+ currentDate: Date;
127
+ events: CalendarEvent[];
128
+ selectedCategoryIds: string[];
129
+ categories: Category[];
130
+ language: string;
131
+ /** holidays/observances shown as a label next to the date number */
132
+ holidays?: Holiday[];
133
+ selectedEvent: CalendarEvent | null;
134
+ previewEvent: PreviewEvent | null;
135
+ expandedRows: Set<number>;
136
+ setExpandedRows: (updater: (prev: Set<number>) => Set<number>) => void;
137
+ onEventClick: (event: CalendarEvent, element?: HTMLElement) => void;
138
+ /** open the create modal for a single day (the "+ new" hover button) */
139
+ onAddEventClick: (date: Date, x?: number, y?: number) => void;
140
+ /** open the create modal for a dragged multi-day range */
141
+ onRangeSelect: (start: Date, end: Date, x: number, y: number) => void;
142
+ /** drag an event chip/bar onto another day to move it — for a recurring
143
+ * event, the host is expected to resolve a "this/following/all" scope
144
+ * (e.g. via a dialog) before actually committing the move */
145
+ onEventMove: (event: CalendarEvent, newDate: Date) => void;
146
+ /** drag a non-recurring period bar's start/end edge onto another day to resize it */
147
+ onEventResize: (event: CalendarEvent, edge: 'start' | 'end', newDate: Date) => void;
148
+ }
149
+ declare function MonthView({ currentDate, events, selectedCategoryIds, categories, language, holidays, selectedEvent, previewEvent, expandedRows, setExpandedRows, onEventClick, onAddEventClick, onRangeSelect, onEventMove, onEventResize, }: MonthViewProps): react.JSX.Element;
150
+
151
+ interface WeekViewProps {
152
+ currentDate: Date;
153
+ events: CalendarEvent[];
154
+ selectedCategoryIds: string[];
155
+ categories: Category[];
156
+ language: string;
157
+ selectedEvent: CalendarEvent | null;
158
+ previewEvent: PreviewEvent | null;
159
+ onEventClick: (event: CalendarEvent, element?: HTMLElement) => void;
160
+ onAddEventClick: (date: Date, hour?: number, clientX?: number, clientY?: number) => void;
161
+ /** drag an empty time-grid area (snapped to 15 min) to create a timed event */
162
+ onTimeRangeSelect: (date: Date, startMin: number, endMin: number) => void;
163
+ /** drag (any event) or resize (non-recurring only) to a new start/end
164
+ * time; `newDate` is set only when a "move" drag also crossed into a
165
+ * different day column. For a recurring event, the host is expected to
166
+ * resolve a "this/following/all" scope before actually committing */
167
+ onEventTimeChange: (event: CalendarEvent, startMin: number, endMin: number, newDate?: Date) => void;
168
+ }
169
+ /** WeekView — a 7-day × 24-hour time grid with absolutely-positioned event chips. */
170
+ declare function WeekView({ currentDate, events, selectedCategoryIds, categories, language, selectedEvent, previewEvent, onEventClick, onAddEventClick, onTimeRangeSelect, onEventTimeChange, }: WeekViewProps): react.JSX.Element;
171
+
172
+ interface DayViewProps {
173
+ currentDate: Date;
174
+ events: CalendarEvent[];
175
+ selectedCategoryIds: string[];
176
+ categories: Category[];
177
+ language: string;
178
+ selectedEvent: CalendarEvent | null;
179
+ previewEvent: PreviewEvent | null;
180
+ onEventClick: (event: CalendarEvent, element?: HTMLElement) => void;
181
+ onAddEventClick: (date: Date, hour?: number, clientX?: number, clientY?: number) => void;
182
+ /** drag an empty time-grid area (snapped to 15 min) to create a timed event */
183
+ onTimeRangeSelect: (date: Date, startMin: number, endMin: number) => void;
184
+ /** drag (any event) or resize (non-recurring only) to a new start/end
185
+ * time. For a recurring event, the host is expected to resolve a
186
+ * "this/following/all" scope before actually committing */
187
+ onEventTimeChange: (event: CalendarEvent, startMin: number, endMin: number) => void;
188
+ }
189
+ /** DayView — a single-day × 24-hour time grid. */
190
+ declare function DayView({ currentDate, events, selectedCategoryIds, categories, language, selectedEvent, previewEvent, onEventClick, onAddEventClick, onTimeRangeSelect, onEventTimeChange, }: DayViewProps): react.JSX.Element;
191
+
192
+ interface YearViewProps {
193
+ currentDate: Date;
194
+ events: CalendarEvent[];
195
+ selectedCategoryIds: string[];
196
+ categories: Category[];
197
+ language: string;
198
+ onDateClick: (date: Date) => void;
199
+ onViewChange: (view: 'month') => void;
200
+ }
201
+ /** YearView — a 12-up grid of mini month calendars with per-day event dots. */
202
+ declare function YearView({ currentDate, events, selectedCategoryIds, categories, language, onDateClick, onViewChange, }: YearViewProps): react.JSX.Element;
203
+
204
+ type DeleteType = 'this' | 'following' | 'all';
205
+ interface DeleteOptionsDialogProps {
206
+ language: string;
207
+ selectedDeleteType: DeleteType;
208
+ setSelectedDeleteType: (type: DeleteType) => void;
209
+ onCancel: () => void;
210
+ onDelete: () => void;
211
+ }
212
+ /**
213
+ * DeleteOptionsDialog — the recurring-event delete scope picker (this / this &
214
+ * following / all), shown in a small centered dialog. Ported from calendary.
215
+ */
216
+ declare function DeleteOptionsDialog({ language, selectedDeleteType, setSelectedDeleteType, onCancel, onDelete, }: DeleteOptionsDialogProps): react.JSX.Element;
217
+
218
+ interface EventSaveData {
219
+ title: string;
220
+ startTime: string;
221
+ endTime: string;
222
+ description: string;
223
+ categoryId: string;
224
+ startDate: Date;
225
+ endDate?: Date;
226
+ recurrence?: Recurrence;
227
+ }
228
+ interface EventModalProps {
229
+ open: boolean;
230
+ onOpenChange: (open: boolean) => void;
231
+ /** the event being edited, or null when creating */
232
+ event?: CalendarEvent | null;
233
+ selectedDate: Date | null;
234
+ selectedEndDate?: Date | null;
235
+ defaultStartTime?: string;
236
+ defaultEndTime?: string;
237
+ categories: Category[];
238
+ selectedCategoryIds: string[];
239
+ language: string;
240
+ onSave: (data: EventSaveData) => void;
241
+ onDelete?: (deleteType: DeleteType) => void;
242
+ /** live preview callback (drives the ghost chip in the views) */
243
+ onChange?: (data: EventSaveData | null) => void;
244
+ onToggleCategory: (id: string) => void;
245
+ onAddCategory: (data: {
246
+ name: string;
247
+ color: string;
248
+ }) => string;
249
+ onUpdateCategory: (id: string, data: {
250
+ name: string;
251
+ color: string;
252
+ }) => void;
253
+ onDeleteCategory: (id: string) => void;
254
+ onReorderCategories: (from: number, to: number) => void;
255
+ }
256
+ /**
257
+ * EventModal — the create/edit event dialog. Ported from calendary's
258
+ * EventCreatePopover into a centered token-CSS modal: title, start/end date,
259
+ * start/end time (duration-preserving), a category picker with inline add/edit,
260
+ * a description field, and a recurrence editor. Save / Cancel / Delete footer;
261
+ * deleting a recurring event opens the scope picker.
262
+ */
263
+ declare function EventModal({ open, onOpenChange, event, selectedDate, selectedEndDate, defaultStartTime, defaultEndTime, categories, selectedCategoryIds, language, onSave, onDelete, onChange, onToggleCategory, onAddCategory, onUpdateCategory, onDeleteCategory, onReorderCategories, }: EventModalProps): react.JSX.Element | null;
264
+
265
+ type RecurrenceEndType = 'never' | 'date' | 'count';
266
+ interface RecurrenceSectionProps {
267
+ language: string;
268
+ recurrenceFreq: RecurrenceFreq;
269
+ setRecurrenceFreq: (freq: RecurrenceFreq) => void;
270
+ /** weekdays as Mon=0 … Sun=6 */
271
+ selectedWeekdays: number[];
272
+ setSelectedWeekdays: (weekdays: number[]) => void;
273
+ recurrenceEndType: RecurrenceEndType;
274
+ setRecurrenceEndType: (type: RecurrenceEndType) => void;
275
+ recurrenceCount: string | number;
276
+ setRecurrenceCount: (count: string | number) => void;
277
+ recurrenceEndDate: Date | undefined;
278
+ setRecurrenceEndDate: (date: Date | undefined) => void;
279
+ internalStartDate: Date | undefined;
280
+ }
281
+ /**
282
+ * RecurrenceSection — frequency segmented tabs, a weekly weekday picker, and an
283
+ * end-condition selector (never / date / count). Ported from calendary. Fully
284
+ * controlled; all state lives in the parent event modal.
285
+ */
286
+ declare function RecurrenceSection({ language, recurrenceFreq, setRecurrenceFreq, selectedWeekdays, setSelectedWeekdays, recurrenceEndType, setRecurrenceEndType, recurrenceCount, setRecurrenceCount, setRecurrenceEndDate, internalStartDate, }: RecurrenceSectionProps): react.JSX.Element;
287
+
288
+ interface RecurrenceScopeDialogProps {
289
+ language: string;
290
+ selectedScope: DeleteType;
291
+ setSelectedScope: (type: DeleteType) => void;
292
+ onCancel: () => void;
293
+ onConfirm: () => void;
294
+ }
295
+ /**
296
+ * RecurrenceScopeDialog — the recurring-event drag-move scope picker (this /
297
+ * this & following / all), shown in a small centered dialog. Same shape as
298
+ * DeleteOptionsDialog but a separate component: the confirm action here is
299
+ * "move" (primary button), not "delete" (danger button), and the two flows
300
+ * are independent enough that sharing one component wasn't worth the prop
301
+ * branching.
302
+ */
303
+ declare function RecurrenceScopeDialog({ language, selectedScope, setSelectedScope, onCancel, onConfirm, }: RecurrenceScopeDialogProps): react.JSX.Element;
304
+
305
+ interface CategoryItemProps {
306
+ cat: Category;
307
+ index: number;
308
+ checked: boolean;
309
+ language: string;
310
+ colorPalette: string[];
311
+ /** whether this row is in inline-edit mode */
312
+ editing: boolean;
313
+ onToggle: (id: string) => void;
314
+ onEditStart: (cat: Category) => void;
315
+ onEditCommit: (id: string, data: {
316
+ name: string;
317
+ color: string;
318
+ }) => void;
319
+ onEditCancel: () => void;
320
+ onDelete: (id: string) => void;
321
+ /** native drag reorder wiring */
322
+ onDragStartRow: (index: number) => void;
323
+ onDragEnterRow: (index: number) => void;
324
+ onDropRow: () => void;
325
+ }
326
+ /**
327
+ * CategoryItem — one row in the category list: a color-dot checkbox + name, a
328
+ * hover "⋯" menu (edit / delete), an inline edit mode with a color-swatch
329
+ * popover, and native drag-to-reorder. Ported from calendary's
330
+ * DraggableCategoryItem (Google-calendar branches dropped).
331
+ */
332
+ declare function CategoryItem({ cat, index, checked, language, colorPalette, editing, onToggle, onEditStart, onEditCommit, onEditCancel, onDelete, onDragStartRow, onDragEnterRow, onDropRow, }: CategoryItemProps): react.JSX.Element;
333
+
334
+ interface CategoryFilterProps {
335
+ categories: Category[];
336
+ selectedCategoryIds: string[];
337
+ language: string;
338
+ onToggle: (id: string) => void;
339
+ onAdd: (data: {
340
+ name: string;
341
+ color: string;
342
+ }) => string;
343
+ onUpdate: (id: string, data: {
344
+ name: string;
345
+ color: string;
346
+ }) => void;
347
+ onDelete: (id: string) => void;
348
+ onReorder: (from: number, to: number) => void;
349
+ }
350
+ /**
351
+ * CategoryFilter — the header's "N selected" popover: the multi-select category
352
+ * list (drag-reorderable) plus an inline add row. Ported from calendary's
353
+ * category dropdown.
354
+ */
355
+ declare function CategoryFilter({ categories, selectedCategoryIds, language, onToggle, onAdd, onUpdate, onDelete, onReorder, }: CategoryFilterProps): react.JSX.Element;
356
+
357
+ interface SegmentOption<T extends string> {
358
+ value: T;
359
+ label: ReactNode;
360
+ }
361
+ interface SegmentTabsProps<T extends string> {
362
+ value: T;
363
+ onValueChange: (value: T) => void;
364
+ options: SegmentOption<T>[];
365
+ /** each tab grows to fill (used inside grids/rows). Default false. */
366
+ block?: boolean;
367
+ className?: string;
368
+ }
369
+ /**
370
+ * SegmentTabs — an iOS-style segmented control: a muted track with a raised
371
+ * white pill on the active tab. Ported from calendary's SegmentTabs.
372
+ */
373
+ declare function SegmentTabs<T extends string>({ value, onValueChange, options, block, className, }: SegmentTabsProps<T>): react.JSX.Element;
374
+
375
+ /** Apple-style category color presets (ported from calendary). */
376
+ declare const COLOR_PRESETS: string[];
377
+ /** nearest preset to an arbitrary hex, by RGB euclidean distance */
378
+ declare function findClosestPresetColor(hex: string): string;
379
+ /** hex → rgba at the given alpha (used for the event-chip fill tint) */
380
+ declare function withAlpha(color: string | undefined, alpha: number): string;
381
+
382
+ /**
383
+ * All occurrence *dates* of a recurring series that fall in [from, to]
384
+ * (inclusive), honouring interval / byweekday / until / count. No rrule dep —
385
+ * a compact engine covering DAILY / WEEKLY / MONTHLY / YEARLY. Exported for
386
+ * "this and following" recurrence splits (see Calendar.tsx's
387
+ * handleRecurrenceScopeConfirm), which needs to know how many occurrences
388
+ * land before a given split date to carry over a correct remaining `count`.
389
+ */
390
+ declare function occurrencesInRange(base: Date, rec: Recurrence, from: Date, to: Date): Date[];
391
+ /**
392
+ * Expand every recurring master in `events` into concrete instances that fall
393
+ * inside [startDate, endDate]; non-recurring events pass through unchanged.
394
+ */
395
+ declare function expandRecurringEvents(events: CalendarEvent[], startDate: Date, endDate: Date): CalendarEvent[];
396
+ /** whether `date` falls inside an event's [date, endDate] span (day-granular) */
397
+ declare function eventCoversDate(event: CalendarEvent, date: Date): boolean;
398
+ /** events on a given date, category-filtered, sorted by start time */
399
+ declare function getEventsForDate(date: Date | null, events: CalendarEvent[], selectedCategoryIds: string[]): CalendarEvent[];
400
+ /** first free hour ("HH:00") on a date, 09:00–23:00, else 09:00 */
401
+ declare function getNextAvailableTime(date: Date | null, events: CalendarEvent[]): string;
402
+
403
+ declare const monthNames: Record<string, string[]>;
404
+ declare const dayNames: Record<string, string[]>;
405
+ /** the month grid, including leading/trailing days from adjacent months */
406
+ declare function getDaysInMonth(date: Date): {
407
+ days: Date[];
408
+ rows: number;
409
+ };
410
+ /** the seven days (Sun→Sat) of the week containing `date` */
411
+ declare function getWeekDays(date: Date): Date[];
412
+ declare function isSameDay(a: Date | null, b: Date | null): boolean;
413
+ declare function isToday(date: Date | null): boolean;
414
+ declare function isCurrentMonth(date: Date | null, currentDate: Date): boolean;
415
+ declare function getPreviousPeriodDate(currentDate: Date, viewType: ViewType): Date;
416
+ declare function getNextPeriodDate(currentDate: Date, viewType: ViewType): Date;
417
+ /** YYYY-MM-DD (local) */
418
+ declare function formatDate(date: Date): string;
419
+ /** clientY inside a time-grid column's bounding rect → minutes-of-day (0–1440), unclamped-snap-ready */
420
+ declare function minutesFromY(clientY: number, rect: {
421
+ top: number;
422
+ height: number;
423
+ }): number;
424
+ /** round to the nearest `step` minutes (default 15) */
425
+ declare function snapMinutes(minutes: number, step?: number): number;
426
+ /** minutes-of-day → "HH:MM", clamped to a valid time */
427
+ declare function minutesToHHMM(minutes: number): string;
428
+ /** "HH:MM" → minutes-of-day */
429
+ declare function hhmmToMinutes(hhmm: string | undefined): number;
430
+
431
+ /** The calendar ships ko/en strings inline (like the source), chosen by `language`. */
432
+ type CalLocale = 'ko' | 'en';
433
+
434
+ /**
435
+ * @softium/calendar
436
+ *
437
+ * A full calendar UI (month / week / day / year views + an event create/edit
438
+ * modal with categories and recurrence), token-CSS themed. Import the stylesheet
439
+ * once: import '@softium/calendar/styles.css';
440
+ */
441
+ declare const VERSION = "0.0.0";
442
+
443
+ export { COLOR_PRESETS, type CalLocale, Calendar, type CalendarController, type CalendarEvent, type CalendarProps, type Category, CategoryFilter, type CategoryFilterProps, CategoryItem, type CategoryItemProps, DayView, type DayViewProps, DeleteOptionsDialog, type DeleteOptionsDialogProps, type DeleteType, EventModal, type EventModalProps, type EventSaveData, type Holiday, MonthView, type MonthViewProps, type PreviewEvent, type Recurrence, type RecurrenceEndType, type RecurrenceFreq, RecurrenceScopeDialog, type RecurrenceScopeDialogProps, RecurrenceSection, type RecurrenceSectionProps, type SegmentOption, SegmentTabs, type SegmentTabsProps, type UseCalendarOptions, VERSION, type ViewType, WeekView, type WeekViewProps, YearView, type YearViewProps, dayNames, eventCoversDate, expandRecurringEvents, findClosestPresetColor, formatDate, getDaysInMonth, getEventsForDate, getNextAvailableTime, getNextPeriodDate, getPreviousPeriodDate, getWeekDays, hhmmToMinutes, isCurrentMonth, isSameDay, isToday, minutesFromY, minutesToHHMM, monthNames, occurrencesInRange, snapMinutes, useCalendar, withAlpha };