@hectorbliss/denik-calendar 0.0.2 → 0.0.4

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.d.ts CHANGED
@@ -1,6 +1,34 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, RefObject } from 'react';
3
3
 
4
+ /**
5
+ * Resource definition for day/resource view mode
6
+ * Use this to represent courts, rooms, employees, etc.
7
+ */
8
+ interface Resource {
9
+ /** Unique identifier for the resource */
10
+ id: string;
11
+ /** Display name */
12
+ name: string;
13
+ /** Optional icon/avatar */
14
+ icon?: ReactNode;
15
+ }
16
+ /**
17
+ * Props passed to custom column header renderer
18
+ * Use this to build custom headers for resources (courts, rooms, employees, etc.)
19
+ */
20
+ interface ColumnHeaderProps {
21
+ /** The date for this column */
22
+ date: Date;
23
+ /** Column index */
24
+ index: number;
25
+ /** Whether this column represents today */
26
+ isToday: boolean;
27
+ /** The configured locale */
28
+ locale: string;
29
+ /** Resource data (only in resource mode) */
30
+ resource?: Resource;
31
+ }
4
32
  /**
5
33
  * Generic calendar event - decoupled from any ORM
6
34
  */
@@ -13,6 +41,8 @@ interface CalendarEvent {
13
41
  service?: {
14
42
  name: string;
15
43
  } | null;
44
+ /** Resource ID for day/resource view (court, room, etc.) */
45
+ resourceId?: string;
16
46
  }
17
47
  /**
18
48
  * Calendar configuration options
@@ -30,6 +60,23 @@ interface CalendarConfig {
30
60
  edit?: ReactNode;
31
61
  close?: ReactNode;
32
62
  };
63
+ /**
64
+ * Custom renderer for column headers.
65
+ * Use this to display resources (courts, rooms, employees) instead of weekdays.
66
+ *
67
+ * @example
68
+ * // Padel courts
69
+ * renderColumnHeader: ({ index }) => <span>Court {index + 1}</span>
70
+ *
71
+ * @example
72
+ * // With custom styling
73
+ * renderColumnHeader: ({ date, isToday }) => (
74
+ * <div className={isToday ? "font-bold" : ""}>
75
+ * {date.toLocaleDateString("en", { weekday: "short" })}
76
+ * </div>
77
+ * )
78
+ */
79
+ renderColumnHeader?: (props: ColumnHeaderProps) => ReactNode;
33
80
  }
34
81
  /**
35
82
  * Calendar component props
@@ -39,6 +86,18 @@ interface CalendarProps {
39
86
  date?: Date;
40
87
  /** Array of events to display */
41
88
  events?: CalendarEvent[];
89
+ /**
90
+ * Resources for day/resource view mode.
91
+ * When provided, columns represent resources instead of weekdays.
92
+ * Events are filtered by date and grouped by resourceId.
93
+ *
94
+ * @example
95
+ * resources={[
96
+ * { id: "court-1", name: "Cancha 1", icon: <PadelIcon /> },
97
+ * { id: "court-2", name: "Cancha 2", icon: <PadelIcon /> },
98
+ * ]}
99
+ */
100
+ resources?: Resource[];
42
101
  /** Callback when an event is clicked */
43
102
  onEventClick?: (event: CalendarEvent) => void;
44
103
  /** Callback when an event is moved via drag & drop */
@@ -53,7 +112,92 @@ interface CalendarProps {
53
112
  config?: CalendarConfig;
54
113
  }
55
114
 
56
- declare function Calendar({ date, events, onEventClick, onNewEvent, onEventMove, onAddBlock, onRemoveBlock, config, }: CalendarProps): react_jsx_runtime.JSX.Element;
115
+ declare function Calendar({ date, events, resources, onEventClick, onNewEvent, onEventMove, onAddBlock, onRemoveBlock, config, }: CalendarProps): react_jsx_runtime.JSX.Element;
116
+
117
+ type CalendarView = "week" | "day";
118
+ interface UseCalendarControlsOptions {
119
+ /** Initial date (default: today) */
120
+ initialDate?: Date;
121
+ /** Initial view mode (default: "week") */
122
+ initialView?: CalendarView;
123
+ /** Locale for date formatting (default: "es-MX") */
124
+ locale?: string;
125
+ }
126
+ interface CalendarControls$1 {
127
+ /** Current date */
128
+ date: Date;
129
+ /** Current view mode */
130
+ view: CalendarView;
131
+ /** Week days array (Mon-Sun) */
132
+ week: Date[];
133
+ /** Formatted label for current date/week */
134
+ label: string;
135
+ /** Navigate to today */
136
+ goToToday: () => void;
137
+ /** Navigate to previous period (week or day) */
138
+ goToPrev: () => void;
139
+ /** Navigate to next period (week or day) */
140
+ goToNext: () => void;
141
+ /** Toggle between week and day view */
142
+ toggleView: () => void;
143
+ /** Set specific date */
144
+ setDate: (date: Date) => void;
145
+ /** Set specific view */
146
+ setView: (view: CalendarView) => void;
147
+ /** Check if current date is today */
148
+ isToday: boolean;
149
+ }
150
+ /**
151
+ * Hook for calendar navigation controls
152
+ *
153
+ * @example
154
+ * const controls = useCalendarControls();
155
+ *
156
+ * <button onClick={controls.goToToday}>HOY</button>
157
+ * <button onClick={controls.goToPrev}>←</button>
158
+ * <button onClick={controls.goToNext}>→</button>
159
+ * <span>{controls.label}</span>
160
+ *
161
+ * <Calendar
162
+ * date={controls.date}
163
+ * resources={controls.view === "day" ? courts : undefined}
164
+ * />
165
+ */
166
+ declare function useCalendarControls(options?: UseCalendarControlsOptions): CalendarControls$1;
167
+
168
+ interface CalendarControlsProps {
169
+ /** Controls from useCalendarControls hook */
170
+ controls: CalendarControls$1;
171
+ /** Custom "Today" button label */
172
+ todayLabel?: string;
173
+ /** Custom "Week" label */
174
+ weekLabel?: string;
175
+ /** Custom "Day" label */
176
+ dayLabel?: string;
177
+ /** Show view toggle (default: true) */
178
+ showViewToggle?: boolean;
179
+ /** Custom prev icon */
180
+ prevIcon?: ReactNode;
181
+ /** Custom next icon */
182
+ nextIcon?: ReactNode;
183
+ /** Additional action buttons (export, add, etc.) */
184
+ actions?: ReactNode;
185
+ /** Custom class name */
186
+ className?: string;
187
+ }
188
+ /**
189
+ * Pre-built calendar controls component
190
+ *
191
+ * @example
192
+ * const controls = useCalendarControls();
193
+ *
194
+ * <CalendarControls
195
+ * controls={controls}
196
+ * actions={<button>Add Event</button>}
197
+ * />
198
+ * <Calendar date={controls.date} />
199
+ */
200
+ declare function CalendarControls({ controls, todayLabel, weekLabel, dayLabel, showViewToggle, prevIcon, nextIcon, actions, className, }: CalendarControlsProps): react_jsx_runtime.JSX.Element;
57
201
 
58
202
  /**
59
203
  * Hook for managing calendar events - overlap detection, filtering, and availability
@@ -128,4 +272,4 @@ declare function useClickOutside<T extends HTMLElement>({ isActive, onOutsideCli
128
272
  */
129
273
  declare function formatDate(date: Date, locale?: string): string;
130
274
 
131
- export { Calendar, type CalendarConfig, type CalendarEvent, type CalendarProps, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarEvents, useClickOutside, useEventOverlap };
275
+ export { Calendar, type CalendarConfig, CalendarControls, type CalendarControlsProps, type CalendarControls$1 as CalendarControlsState, type CalendarEvent, type CalendarProps, type CalendarView, type ColumnHeaderProps, type Resource, Calendar as SimpleBigWeekView, type UseCalendarControlsOptions, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarControls, useCalendarEvents, useClickOutside, useEventOverlap };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useEffect, useState } from 'react';
1
+ import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
2
2
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
3
3
  import { CSS } from '@dnd-kit/utilities';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -215,21 +215,40 @@ var cn = (...classes) => classes.filter(Boolean).join(" ");
215
215
  var DefaultTrashIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }) });
216
216
  var DefaultEditIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" }) });
217
217
  var DefaultCloseIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) });
218
- var DayHeader = ({ date, locale }) => /* @__PURE__ */ jsxs("p", { className: "grid place-items-center", children: [
219
- /* @__PURE__ */ jsx("span", { className: "capitalize", children: date.toLocaleDateString(locale, { weekday: "short" }) }),
220
- /* @__PURE__ */ jsx(
221
- "span",
222
- {
223
- className: cn(
224
- isToday(date) && "bg-blue-500 rounded-full p-1 text-white"
225
- ),
226
- children: date.getDate()
227
- }
228
- )
229
- ] });
218
+ var DayHeader = ({
219
+ date,
220
+ locale,
221
+ index,
222
+ resource,
223
+ renderColumnHeader
224
+ }) => {
225
+ const isToday2 = isToday(date);
226
+ if (renderColumnHeader) {
227
+ return /* @__PURE__ */ jsx("div", { className: "grid place-items-center", children: renderColumnHeader({ date, index, isToday: isToday2, locale, resource }) });
228
+ }
229
+ if (resource) {
230
+ return /* @__PURE__ */ jsxs("div", { className: "grid place-items-center gap-1", children: [
231
+ resource.icon && /* @__PURE__ */ jsx("div", { className: "w-8 h-8 flex items-center justify-center", children: resource.icon }),
232
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: resource.name })
233
+ ] });
234
+ }
235
+ return /* @__PURE__ */ jsxs("p", { className: "grid place-items-center", children: [
236
+ /* @__PURE__ */ jsx("span", { className: "capitalize", children: date.toLocaleDateString(locale, { weekday: "short" }) }),
237
+ /* @__PURE__ */ jsx(
238
+ "span",
239
+ {
240
+ className: cn(
241
+ isToday2 && "bg-blue-500 rounded-full p-1 text-white"
242
+ ),
243
+ children: date.getDate()
244
+ }
245
+ )
246
+ ] });
247
+ };
230
248
  function Calendar({
231
249
  date = /* @__PURE__ */ new Date(),
232
250
  events = [],
251
+ resources,
233
252
  onEventClick,
234
253
  onNewEvent,
235
254
  onEventMove,
@@ -237,10 +256,12 @@ function Calendar({
237
256
  onRemoveBlock,
238
257
  config = {}
239
258
  }) {
240
- const { locale = "es-MX", icons = {} } = config;
259
+ const { locale = "es-MX", icons = {}, renderColumnHeader } = config;
241
260
  const week = completeWeek(date);
242
261
  const [activeId, setActiveId] = useState(null);
243
262
  const { canMove } = useCalendarEvents(events);
263
+ const isResourceMode = !!resources && resources.length > 0;
264
+ const columnCount = isResourceMode ? resources.length : 7;
244
265
  const sensors = useSensors(
245
266
  useSensor(PointerSensor, {
246
267
  activationConstraint: { distance: 8 }
@@ -255,10 +276,10 @@ function Calendar({
255
276
  setActiveId(null);
256
277
  if (!over) return;
257
278
  const eventId = active.id.toString().replace("event-", "");
258
- const [, dayIndexStr, hourStr] = over.id.toString().split("-");
259
- const dayIndex = parseInt(dayIndexStr);
279
+ const [, colIndexStr, hourStr] = over.id.toString().split("-");
280
+ const colIndex = parseInt(colIndexStr);
260
281
  const hour = parseInt(hourStr);
261
- const targetDay = week[dayIndex];
282
+ const targetDay = isResourceMode ? date : week[colIndex];
262
283
  const newStart = new Date(targetDay);
263
284
  newStart.setHours(hour, 0, 0, 0);
264
285
  const movedEvent = events.find((e) => e.id === eventId);
@@ -277,6 +298,24 @@ function Calendar({
277
298
  setActiveId(null);
278
299
  };
279
300
  const activeEvent = activeId ? events.find((e) => `event-${e.id}` === activeId) : null;
301
+ const getColumnEvents = (colIndex) => {
302
+ if (isResourceMode) {
303
+ const resourceId = resources[colIndex].id;
304
+ return events.filter((event) => {
305
+ const eventDate = new Date(event.start);
306
+ return event.resourceId === resourceId && areSameDates(eventDate, date);
307
+ });
308
+ }
309
+ const dayOfWeek = week[colIndex];
310
+ return events.filter((event) => {
311
+ const eventDate = new Date(event.start);
312
+ return eventDate.getDate() === dayOfWeek.getDate() && eventDate.getMonth() === dayOfWeek.getMonth();
313
+ });
314
+ };
315
+ const gridStyle = {
316
+ display: "grid",
317
+ gridTemplateColumns: `auto repeat(${columnCount}, minmax(120px, 1fr))`
318
+ };
280
319
  return /* @__PURE__ */ jsxs(
281
320
  DndContext,
282
321
  {
@@ -286,32 +325,66 @@ function Calendar({
286
325
  onDragEnd: handleDragEnd,
287
326
  onDragCancel: handleDragCancel,
288
327
  children: [
289
- /* @__PURE__ */ jsxs("article", { className: "w-full bg-white shadow rounded-xl", children: [
290
- /* @__PURE__ */ jsxs("section", { className: "grid grid-cols-8 place-items-center py-4", children: [
291
- /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-500", children: Intl.DateTimeFormat().resolvedOptions().timeZone }) }),
292
- week.map((day) => /* @__PURE__ */ jsx(DayHeader, { date: day, locale }, day.toISOString()))
293
- ] }),
294
- /* @__PURE__ */ jsxs("section", { className: "grid grid-cols-8 max-h-[80vh] overflow-y-auto", children: [
295
- /* @__PURE__ */ jsx(TimeColumn, {}),
296
- week.map((dayOfWeek, dayIndex) => /* @__PURE__ */ jsx(
297
- Column,
298
- {
299
- dayIndex,
300
- dayOfWeek,
301
- events: events.filter((event) => {
302
- const eventDate = new Date(event.start);
303
- return eventDate.getDate() === dayOfWeek.getDate() && eventDate.getMonth() === dayOfWeek.getMonth();
304
- }),
305
- onNewEvent,
306
- onAddBlock,
307
- onRemoveBlock,
308
- onEventClick,
309
- locale,
310
- icons
311
- },
312
- dayOfWeek.toISOString()
313
- ))
314
- ] })
328
+ /* @__PURE__ */ jsxs("article", { className: "w-full bg-white shadow rounded-xl overflow-hidden", children: [
329
+ /* @__PURE__ */ jsxs(
330
+ "section",
331
+ {
332
+ style: gridStyle,
333
+ className: "place-items-center py-4 border-b",
334
+ children: [
335
+ /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-500", children: Intl.DateTimeFormat().resolvedOptions().timeZone }) }),
336
+ isResourceMode ? resources.map((resource, index) => /* @__PURE__ */ jsx(
337
+ DayHeader,
338
+ {
339
+ date,
340
+ locale,
341
+ index,
342
+ resource,
343
+ renderColumnHeader
344
+ },
345
+ resource.id
346
+ )) : week.map((day, index) => /* @__PURE__ */ jsx(
347
+ DayHeader,
348
+ {
349
+ date: day,
350
+ locale,
351
+ index,
352
+ renderColumnHeader
353
+ },
354
+ day.toISOString()
355
+ ))
356
+ ]
357
+ }
358
+ ),
359
+ /* @__PURE__ */ jsxs(
360
+ "section",
361
+ {
362
+ style: gridStyle,
363
+ className: cn(
364
+ "max-h-[80vh] overflow-y-auto",
365
+ isResourceMode && "overflow-x-auto"
366
+ ),
367
+ children: [
368
+ /* @__PURE__ */ jsx(TimeColumn, {}),
369
+ Array.from({ length: columnCount }, (_, colIndex) => /* @__PURE__ */ jsx(
370
+ Column,
371
+ {
372
+ dayIndex: colIndex,
373
+ dayOfWeek: isResourceMode ? date : week[colIndex],
374
+ events: getColumnEvents(colIndex),
375
+ onNewEvent,
376
+ onAddBlock,
377
+ onRemoveBlock,
378
+ onEventClick,
379
+ locale,
380
+ icons,
381
+ resourceId: isResourceMode ? resources[colIndex].id : void 0
382
+ },
383
+ isResourceMode ? resources[colIndex].id : week[colIndex].toISOString()
384
+ ))
385
+ ]
386
+ }
387
+ )
315
388
  ] }),
316
389
  /* @__PURE__ */ jsx(DragOverlay, { children: activeEvent ? /* @__PURE__ */ jsx(EventOverlay, { event: activeEvent }) : null })
317
390
  ]
@@ -409,6 +482,42 @@ var EmptyButton = ({
409
482
  }
410
483
  );
411
484
  };
485
+ var calculateOverlapPositions = (events) => {
486
+ if (events.length === 0) return [];
487
+ const sorted = [...events].sort(
488
+ (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
489
+ );
490
+ const positions = /* @__PURE__ */ new Map();
491
+ const groups = [];
492
+ let currentGroup = [];
493
+ let groupEnd = 0;
494
+ for (const event of sorted) {
495
+ const eventStart = new Date(event.start);
496
+ const startHour = eventStart.getHours() + eventStart.getMinutes() / 60;
497
+ const endHour = startHour + event.duration / 60;
498
+ if (currentGroup.length === 0 || startHour < groupEnd) {
499
+ currentGroup.push(event);
500
+ groupEnd = Math.max(groupEnd, endHour);
501
+ } else {
502
+ groups.push(currentGroup);
503
+ currentGroup = [event];
504
+ groupEnd = endHour;
505
+ }
506
+ }
507
+ if (currentGroup.length > 0) {
508
+ groups.push(currentGroup);
509
+ }
510
+ for (const group of groups) {
511
+ const totalColumns = group.length;
512
+ group.forEach((event, index) => {
513
+ positions.set(event.id, { column: index, totalColumns });
514
+ });
515
+ }
516
+ return sorted.map((event) => ({
517
+ event,
518
+ ...positions.get(event.id)
519
+ }));
520
+ };
412
521
  var Column = ({
413
522
  onEventClick,
414
523
  events = [],
@@ -418,9 +527,11 @@ var Column = ({
418
527
  onRemoveBlock,
419
528
  dayIndex,
420
529
  locale,
421
- icons
530
+ icons,
531
+ resourceId
422
532
  }) => {
423
533
  const columnRef = useRef(null);
534
+ const eventsWithPositions = calculateOverlapPositions(events);
424
535
  useEffect(() => {
425
536
  if (!columnRef.current) return;
426
537
  const today = /* @__PURE__ */ new Date();
@@ -434,21 +545,24 @@ var Column = ({
434
545
  }, 100);
435
546
  }
436
547
  }, [dayOfWeek]);
437
- const findEvent = (hours) => {
438
- const eventStartsHere = events.find(
439
- (event) => new Date(event.start).getHours() === hours
548
+ const findEventsAtHour = (hours) => {
549
+ const eventsStartingHere = eventsWithPositions.filter(
550
+ ({ event }) => new Date(event.start).getHours() === hours
440
551
  );
441
- if (eventStartsHere) {
442
- return /* @__PURE__ */ jsx(
552
+ if (eventsStartingHere.length > 0) {
553
+ return eventsStartingHere.map(({ event, column, totalColumns }) => /* @__PURE__ */ jsx(
443
554
  DraggableEvent,
444
555
  {
445
- onClick: () => onEventClick?.(eventStartsHere),
446
- event: eventStartsHere,
556
+ onClick: () => onEventClick?.(event),
557
+ event,
447
558
  onRemoveBlock,
448
559
  locale,
449
- icons
450
- }
451
- );
560
+ icons,
561
+ overlapColumn: column,
562
+ overlapTotal: totalColumns
563
+ },
564
+ event.id
565
+ ));
452
566
  }
453
567
  const eventSpansHere = events.find((event) => {
454
568
  const eventStart = new Date(event.start);
@@ -474,7 +588,7 @@ var Column = ({
474
588
  date: dayOfWeek,
475
589
  className: "relative",
476
590
  dayIndex,
477
- children: findEvent(hours)
591
+ children: findEventsAtHour(hours)
478
592
  },
479
593
  hours
480
594
  )) });
@@ -485,16 +599,22 @@ var DraggableEvent = ({
485
599
  onClick,
486
600
  onRemoveBlock,
487
601
  locale,
488
- icons
602
+ icons,
603
+ overlapColumn = 0,
604
+ overlapTotal = 1
489
605
  }) => {
490
606
  const [showOptions, setShowOptions] = useState(false);
491
607
  const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
492
608
  id: `event-${event.id}`,
493
609
  disabled: event.type === "BLOCK"
494
610
  });
611
+ const widthPercent = event.type === "BLOCK" ? 100 : 90 / overlapTotal;
612
+ const leftPercent = event.type === "BLOCK" ? 0 : overlapColumn * (90 / overlapTotal);
495
613
  const style = {
496
614
  height: event.duration / 60 * 64,
497
- transform: transform ? CSS.Translate.toString(transform) : void 0
615
+ transform: transform ? CSS.Translate.toString(transform) : void 0,
616
+ width: `${widthPercent}%`,
617
+ left: `${leftPercent}%`
498
618
  };
499
619
  return /* @__PURE__ */ jsxs(Fragment, { children: [
500
620
  /* @__PURE__ */ jsxs(
@@ -507,7 +627,7 @@ var DraggableEvent = ({
507
627
  ...attributes,
508
628
  className: cn(
509
629
  "border grid gap-y-1 overflow-hidden place-content-start",
510
- "text-xs text-left pl-1 absolute top-0 left-0 bg-blue-500 text-white rounded-md z-10 w-[90%]",
630
+ "text-xs text-left pl-1 absolute top-0 bg-blue-500 text-white rounded-md z-10",
511
631
  event.type === "BLOCK" && "bg-gray-300 h-full w-full text-center cursor-not-allowed relative p-0",
512
632
  event.type !== "BLOCK" && "cursor-grab",
513
633
  isDragging && event.type !== "BLOCK" && "cursor-grabbing opacity-50"
@@ -605,5 +725,132 @@ var Options = ({
605
725
  }
606
726
  );
607
727
  };
728
+ var DefaultPrevIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-5 h-5", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) });
729
+ var DefaultNextIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-5 h-5", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" }) });
730
+ function CalendarControls({
731
+ controls,
732
+ todayLabel = "HOY",
733
+ weekLabel = "SEMANA",
734
+ dayLabel = "D\xCDA",
735
+ showViewToggle = true,
736
+ prevIcon,
737
+ nextIcon,
738
+ actions,
739
+ className = ""
740
+ }) {
741
+ const { label, goToToday, goToPrev, goToNext, view, toggleView, isToday: isToday2 } = controls;
742
+ return /* @__PURE__ */ jsxs(
743
+ "div",
744
+ {
745
+ className: `flex items-center justify-between gap-4 py-3 ${className}`,
746
+ children: [
747
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
748
+ /* @__PURE__ */ jsx(
749
+ "button",
750
+ {
751
+ onClick: goToToday,
752
+ disabled: isToday2,
753
+ className: `px-4 py-2 text-sm font-medium rounded-full border transition-colors ${isToday2 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "bg-blue-500 text-white hover:bg-blue-600"}`,
754
+ children: todayLabel
755
+ }
756
+ ),
757
+ /* @__PURE__ */ jsx(
758
+ "button",
759
+ {
760
+ onClick: goToPrev,
761
+ className: "p-2 rounded-full hover:bg-gray-100 transition-colors",
762
+ "aria-label": "Previous",
763
+ children: prevIcon ?? /* @__PURE__ */ jsx(DefaultPrevIcon, {})
764
+ }
765
+ ),
766
+ /* @__PURE__ */ jsx(
767
+ "button",
768
+ {
769
+ onClick: goToNext,
770
+ className: "p-2 rounded-full hover:bg-gray-100 transition-colors",
771
+ "aria-label": "Next",
772
+ children: nextIcon ?? /* @__PURE__ */ jsx(DefaultNextIcon, {})
773
+ }
774
+ ),
775
+ /* @__PURE__ */ jsx("span", { className: "text-lg font-medium capitalize ml-2", children: label })
776
+ ] }),
777
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
778
+ showViewToggle && /* @__PURE__ */ jsxs(
779
+ "select",
780
+ {
781
+ value: view,
782
+ onChange: toggleView,
783
+ className: "px-4 py-2 text-sm font-medium border rounded-lg bg-white hover:bg-gray-50 cursor-pointer",
784
+ children: [
785
+ /* @__PURE__ */ jsx("option", { value: "week", children: weekLabel }),
786
+ /* @__PURE__ */ jsx("option", { value: "day", children: dayLabel })
787
+ ]
788
+ }
789
+ ),
790
+ actions
791
+ ] })
792
+ ]
793
+ }
794
+ );
795
+ }
796
+ function useCalendarControls(options = {}) {
797
+ const {
798
+ initialDate = /* @__PURE__ */ new Date(),
799
+ initialView = "week",
800
+ locale = "es-MX"
801
+ } = options;
802
+ const [date, setDate] = useState(initialDate);
803
+ const [view, setView] = useState(initialView);
804
+ const week = useMemo(() => completeWeek(date), [date]);
805
+ const goToToday = useCallback(() => {
806
+ setDate(/* @__PURE__ */ new Date());
807
+ }, []);
808
+ const goToPrev = useCallback(() => {
809
+ setDate((d) => addDaysToDate(d, view === "week" ? -7 : -1));
810
+ }, [view]);
811
+ const goToNext = useCallback(() => {
812
+ setDate((d) => addDaysToDate(d, view === "week" ? 7 : 1));
813
+ }, [view]);
814
+ const toggleView = useCallback(() => {
815
+ setView((v) => v === "week" ? "day" : "week");
816
+ }, []);
817
+ const isToday2 = useMemo(() => {
818
+ const today = /* @__PURE__ */ new Date();
819
+ return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
820
+ }, [date]);
821
+ const label = useMemo(() => {
822
+ if (view === "week") {
823
+ const monthName = week[0].toLocaleDateString(locale, { month: "long" });
824
+ const year = week[0].getFullYear();
825
+ const endMonth = week[6].getMonth();
826
+ if (week[0].getMonth() !== endMonth) {
827
+ const endMonthName = week[6].toLocaleDateString(locale, {
828
+ month: "short"
829
+ });
830
+ return `${week[0].getDate()} ${week[0].toLocaleDateString(locale, { month: "short" })} - ${week[6].getDate()} ${endMonthName} ${year}`;
831
+ }
832
+ return `${week[0].getDate()} - ${week[6].getDate()} ${monthName} ${year}`;
833
+ }
834
+ return date.toLocaleDateString(locale, {
835
+ weekday: "long",
836
+ day: "numeric",
837
+ month: "long",
838
+ year: "numeric"
839
+ });
840
+ }, [view, week, date, locale]);
841
+ return {
842
+ date,
843
+ view,
844
+ week,
845
+ label,
846
+ goToToday,
847
+ goToPrev,
848
+ goToNext,
849
+ toggleView,
850
+ setDate,
851
+ setView,
852
+ isToday: isToday2
853
+ };
854
+ }
608
855
 
609
- export { Calendar, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarEvents, useClickOutside, useEventOverlap };
856
+ export { Calendar, CalendarControls, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarControls, useCalendarEvents, useClickOutside, useEventOverlap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hectorbliss/denik-calendar",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "description": "A React drag-and-drop week calendar component with overlap detection. Built with @dnd-kit.",
6
6
  "type": "module",