@adamosuiteservices/ui 2.13.2 → 2.13.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.
Files changed (35) hide show
  1. package/dist/components/ui/slider/slider.d.ts +5 -2
  2. package/dist/slider.cjs +7 -8
  3. package/dist/slider.js +192 -178
  4. package/dist/styles.css +1 -1
  5. package/docs/AI-GUIDE.md +321 -321
  6. package/docs/components/layout/sidebar.md +399 -399
  7. package/docs/components/layout/toaster.md +436 -436
  8. package/docs/components/ui/accordion-rounded.md +584 -584
  9. package/docs/components/ui/accordion.md +269 -269
  10. package/docs/components/ui/calendar.md +1159 -1159
  11. package/docs/components/ui/card.md +1455 -1455
  12. package/docs/components/ui/checkbox.md +292 -292
  13. package/docs/components/ui/collapsible.md +323 -323
  14. package/docs/components/ui/dialog.md +628 -628
  15. package/docs/components/ui/field.md +706 -706
  16. package/docs/components/ui/hover-card.md +446 -446
  17. package/docs/components/ui/kbd.md +434 -434
  18. package/docs/components/ui/label.md +359 -359
  19. package/docs/components/ui/pagination.md +650 -650
  20. package/docs/components/ui/popover.md +536 -536
  21. package/docs/components/ui/progress.md +182 -182
  22. package/docs/components/ui/radio-group.md +311 -311
  23. package/docs/components/ui/separator.md +214 -214
  24. package/docs/components/ui/sheet.md +174 -174
  25. package/docs/components/ui/skeleton.md +140 -140
  26. package/docs/components/ui/slider.md +460 -341
  27. package/docs/components/ui/spinner.md +170 -170
  28. package/docs/components/ui/switch.md +408 -408
  29. package/docs/components/ui/tabs-underline.md +106 -106
  30. package/docs/components/ui/tabs.md +122 -122
  31. package/docs/components/ui/textarea.md +243 -243
  32. package/docs/components/ui/toggle.md +237 -237
  33. package/docs/components/ui/tooltip.md +317 -317
  34. package/docs/components/ui/typography.md +320 -320
  35. package/package.json +1 -1
@@ -1,1159 +1,1159 @@
1
- # Calendar
2
-
3
- Calendario interactivo altamente personalizable basado en `react-day-picker` para selección de fechas individuales, múltiples o rangos. Soporta navegación con dropdowns, múltiples meses, números de semana, timezones y fechas deshabilitadas.
4
-
5
- ## Características Principales
6
-
7
- - **4 modos de selección**: Single (fecha única), Multiple (múltiples fechas), Range (rango de fechas), Default (sin selección)
8
- - **Navegación flexible**: Botones tradicionales o dropdowns mes/año para saltos rápidos
9
- - **Múltiples meses**: Visualización simultánea de 2+ meses para rangos amplios
10
- - **Números de semana**: Muestra semana del año (ISO 8601)
11
- - **Fechas deshabilitadas**: Bloquea fechas específicas o rangos completos
12
- - **Timezone awareness**: Soporte completo para zonas horarias
13
- - **Variantes de botones**: Personaliza navegación (ghost, outline, secondary, etc.)
14
- - **RTL support**: Rotación automática de chevrons en idiomas right-to-left
15
- - **Integración con Card/Popover**: Background transparente automático
16
- - **Accesibilidad**: Navegación por teclado, ARIA labels, focus visible
17
-
18
- ## Importación
19
-
20
- ```tsx
21
- import { Calendar } from "@adamosuiteservices/ui/calendar";
22
- import type { DateRange } from "react-day-picker";
23
- ```
24
-
25
- ```tsx
26
- import { Calendar } from "@adamosuiteservices/ui/calendar";
27
- import type { DateRange } from "react-day-picker";
28
- ```
29
-
30
- ## Uso Básico
31
-
32
- ### Selección de Fecha Única
33
-
34
- El modo más común para seleccionar una sola fecha.
35
-
36
- ```tsx
37
- import { useState } from "react";
38
- import { Calendar } from "@adamosuiteservices/ui/calendar";
39
-
40
- function DatePicker() {
41
- const [date, setDate] = useState<Date | undefined>(new Date());
42
-
43
- return (
44
- <Calendar
45
- mode="single"
46
- selected={date}
47
- onSelect={setDate}
48
- className="rounded-md border"
49
- />
50
- );
51
- }
52
- ```
53
-
54
- ### Navegación con Dropdowns
55
-
56
- Para saltar rápidamente entre meses y años.
57
-
58
- ```tsx
59
- const [date, setDate] = useState<Date | undefined>(new Date(2025, 5, 12));
60
-
61
- <Calendar
62
- mode="single"
63
- selected={date}
64
- onSelect={setDate}
65
- captionLayout="dropdown"
66
- className="rounded-md border"
67
- />;
68
- ```
69
-
70
- **Ventaja**: Permite seleccionar fechas lejanas sin hacer múltiples clics en flechas.
71
-
72
- ### Rango de Fechas
73
-
74
- Para seleccionar periodo entre dos fechas (ej: reservas de hotel).
75
-
76
- ```tsx
77
- import type { DateRange } from "react-day-picker";
78
-
79
- function RangePicker() {
80
- const [range, setRange] = useState<DateRange | undefined>({
81
- from: new Date(2025, 5, 12),
82
- to: new Date(2025, 5, 18),
83
- });
84
-
85
- return (
86
- <Calendar
87
- mode="range"
88
- defaultMonth={range?.from}
89
- selected={range}
90
- onSelect={setRange}
91
- className="rounded-md border"
92
- />
93
- );
94
- }
95
- ```
96
-
97
- ### Múltiples Meses
98
-
99
- Ideal para rangos largos o visualización amplia.
100
-
101
- ```tsx
102
- const [range, setRange] = useState<DateRange | undefined>({
103
- from: new Date(2025, 5, 12),
104
- to: new Date(2025, 6, 15),
105
- });
106
-
107
- <Calendar
108
- mode="range"
109
- defaultMonth={range?.from}
110
- selected={range}
111
- onSelect={setRange}
112
- numberOfMonths={2}
113
- className="rounded-md border"
114
- />;
115
- ```
116
-
117
- **Nota**: En pantallas pequeñas, los meses se apilan verticalmente automáticamente.
118
-
119
- ### Selección Múltiple
120
-
121
- Para seleccionar fechas no consecutivas (ej: días de entrenamiento).
122
-
123
- ```tsx
124
- const [dates, setDates] = useState<Date[] | undefined>([
125
- new Date(2025, 5, 12),
126
- new Date(2025, 5, 15),
127
- new Date(2025, 5, 20),
128
- ]);
129
-
130
- <Calendar
131
- mode="multiple"
132
- selected={dates}
133
- onSelect={setDates}
134
- className="rounded-md border"
135
- />;
136
- ```
137
-
138
- ## Componentes
139
-
140
- ### Calendar
141
-
142
- El componente principal que renderiza el calendario completo.
143
-
144
- #### Props
145
-
146
- | Prop | Tipo | Default | Descripción |
147
- | ----------------- | ---------------------------------------------------------------- | ------------- | ------------------------------------------- |
148
- | `mode` | `"single" \| "multiple" \| "range" \| "default"` | `"default"` | Modo de selección de fechas |
149
- | `selected` | `Date \| Date[] \| DateRange \| undefined` | `undefined` | Fecha(s) seleccionada(s) según modo |
150
- | `onSelect` | `(date) => void` | - | Callback cuando cambia la selección |
151
- | `defaultMonth` | `Date` | Mes actual | Mes inicial a mostrar |
152
- | `numberOfMonths` | `number` | `1` | Cantidad de meses a mostrar simultáneamente |
153
- | `captionLayout` | `"label" \| "dropdown" \| "dropdown-months" \| "dropdown-years"` | `"label"` | Tipo de navegación del header |
154
- | `showOutsideDays` | `boolean` | `true` | Mostrar días de meses adyacentes |
155
- | `showWeekNumber` | `boolean` | `false` | Mostrar números de semana |
156
- | `disabled` | `Date \| Date[] \| DateRange \| ((date: Date) => boolean)` | - | Fechas deshabilitadas |
157
- | `hidden` | `Date \| Date[] \| DateRange \| ((date: Date) => boolean)` | - | Fechas ocultas completamente |
158
- | `fromDate` | `Date` | - | Fecha mínima seleccionable |
159
- | `toDate` | `Date` | - | Fecha máxima seleccionable |
160
- | `fromMonth` | `Date` | - | Mes mínimo navegable |
161
- | `toMonth` | `Date` | - | Mes máximo navegable |
162
- | `fromYear` | `number` | - | Año mínimo navegable |
163
- | `toYear` | `number` | - | Año máximo navegable |
164
- | `timeZone` | `string` | Sistema | Zona horaria (ej: `"America/New_York"`) |
165
- | `locale` | `Locale` | Sistema | Objeto Locale de `date-fns` para i18n |
166
- | `weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `0` | Día inicio de semana (0=Domingo, 1=Lunes) |
167
- | `buttonVariant` | `ButtonProps["variant"]` | `"secondary"` | Variante para botones de navegación |
168
- | `className` | `string` | - | Clases CSS adicionales para el contenedor |
169
- | `classNames` | `ClassNames` | - | Objeto con clases para elementos internos |
170
- | `components` | `Components` | - | Componentes custom para override |
171
- | `formatters` | `Formatters` | - | Funciones de formateo custom |
172
-
173
- ### CalendarDayButton
174
-
175
- Componente interno que renderiza cada día del calendario. Raramente se usa directamente.
176
-
177
- ## Modos de Selección
178
-
179
- ### Mode: "single"
180
-
181
- Un solo día seleccionado a la vez.
182
-
183
- ```tsx
184
- // Tipo de selected
185
- selected: Date | undefined
186
-
187
- // Callback
188
- onSelect: (date: Date | undefined) => void
189
- ```
190
-
191
- **Uso común**: Fecha de nacimiento, vencimiento de documento, fecha límite.
192
-
193
- ### Mode: "multiple"
194
-
195
- Array de días no consecutivos.
196
-
197
- ```tsx
198
- // Tipo de selected
199
- selected: Date[] | undefined
200
-
201
- // Callback
202
- onSelect: (dates: Date[] | undefined) => void
203
- ```
204
-
205
- **Uso común**: Días de entrenamiento, fechas de eventos recurrentes, días festivos.
206
-
207
- ### Mode: "range"
208
-
209
- Rango continuo entre dos fechas.
210
-
211
- ```tsx
212
- import type { DateRange } from "react-day-picker";
213
-
214
- // Tipo de selected
215
- selected: DateRange | undefined // { from?: Date; to?: Date }
216
-
217
- // Callback
218
- onSelect: (range: DateRange | undefined) => void
219
- ```
220
-
221
- **Uso común**: Reservas de hotel, filtros de reportes, periodos de vacaciones.
222
-
223
- ### Mode: "default"
224
-
225
- Sin selección (solo visualización).
226
-
227
- ```tsx
228
- <Calendar mode="default" />
229
- ```
230
-
231
- **Uso común**: Mostrar calendario estático, vista de disponibilidad sin interacción.
232
-
233
- ## Captionlayout (Navegación)
234
-
235
- ### "label" (Default)
236
-
237
- Navegación tradicional con flechas.
238
-
239
- ```tsx
240
- <Calendar captionLayout="label" />
241
- ```
242
-
243
- **Ventaja**: UI simple, menos espacio vertical.
244
-
245
- ### "dropdown"
246
-
247
- Dropdowns para mes Y año.
248
-
249
- ```tsx
250
- <Calendar captionLayout="dropdown" fromYear={2020} toYear={2030} />
251
- ```
252
-
253
- **Ventaja**: Saltos rápidos a fechas lejanas. Ideal para fecha de nacimiento.
254
-
255
- ### "dropdown-months"
256
-
257
- Solo dropdown de meses, año con flechas.
258
-
259
- ```tsx
260
- <Calendar captionLayout="dropdown-months" />
261
- ```
262
-
263
- ### "dropdown-years"
264
-
265
- Solo dropdown de años, mes con flechas.
266
-
267
- ```tsx
268
- <Calendar captionLayout="dropdown-years" />
269
- ```
270
-
271
- ## Patrones Avanzados
272
-
273
- ### Fechas Deshabilitadas (Múltiples Formas)
274
-
275
- #### Fechas Específicas
276
-
277
- ```tsx
278
- const disabledDays = [
279
- new Date(2025, 9, 15),
280
- new Date(2025, 9, 16),
281
- new Date(2025, 9, 17),
282
- ];
283
-
284
- <Calendar
285
- mode="single"
286
- disabled={disabledDays}
287
- selected={date}
288
- onSelect={setDate}
289
- />;
290
- ```
291
-
292
- #### Rangos de Fechas
293
-
294
- ```tsx
295
- const disabledRanges = [
296
- { from: new Date(2025, 9, 20), to: new Date(2025, 9, 25) },
297
- { from: new Date(2025, 10, 1), to: new Date(2025, 10, 7) },
298
- ];
299
-
300
- <Calendar disabled={disabledRanges} />;
301
- ```
302
-
303
- #### Función Dinámica
304
-
305
- ```tsx
306
- // Deshabilitar fines de semana
307
- const isWeekend = (date: Date) => {
308
- const day = date.getDay();
309
- return day === 0 || day === 6;
310
- };
311
-
312
- <Calendar disabled={isWeekend} />;
313
- ```
314
-
315
- #### Deshabilitar Pasado
316
-
317
- ```tsx
318
- // Solo fechas futuras
319
- <Calendar
320
- disabled={{ before: new Date() }}
321
- mode="single"
322
- selected={date}
323
- onSelect={setDate}
324
- />
325
- ```
326
-
327
- #### Deshabilitar Futuro
328
-
329
- ```tsx
330
- // Solo fechas pasadas
331
- <Calendar
332
- disabled={{ after: new Date() }}
333
- mode="single"
334
- selected={date}
335
- onSelect={setDate}
336
- />
337
- ```
338
-
339
- ### Rango de Fechas Permitidas
340
-
341
- #### Con fromDate y toDate
342
-
343
- ```tsx
344
- // Solo permitir 30 días hacia adelante
345
- <Calendar
346
- mode="single"
347
- fromDate={new Date()}
348
- toDate={new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)}
349
- selected={date}
350
- onSelect={setDate}
351
- />
352
- ```
353
-
354
- #### Con fromYear y toYear
355
-
356
- ```tsx
357
- // Para fecha de nacimiento (mayores de 18)
358
- const currentYear = new Date().getFullYear();
359
-
360
- <Calendar
361
- mode="single"
362
- captionLayout="dropdown"
363
- fromYear={1950}
364
- toYear={currentYear - 18}
365
- defaultMonth={new Date(2000, 0)}
366
- selected={birthdate}
367
- onSelect={setBirthdate}
368
- />;
369
- ```
370
-
371
- ### Calendario en Card
372
-
373
- Integración perfecta con componente Card (background transparente automático).
374
-
375
- ```tsx
376
- import {
377
- Card,
378
- CardContent,
379
- CardDescription,
380
- CardHeader,
381
- CardTitle,
382
- } from "@adamosuiteservices/ui/card";
383
-
384
- <Card className="w-fit">
385
- <CardHeader>
386
- <CardTitle>Select Date</CardTitle>
387
- <CardDescription>Choose a date for your appointment</CardDescription>
388
- </CardHeader>
389
- <CardContent>
390
- <Calendar
391
- mode="single"
392
- selected={date}
393
- onSelect={setDate}
394
- captionLayout="dropdown"
395
- />
396
- </CardContent>
397
- </Card>;
398
- ```
399
-
400
- ### Calendario en Popover
401
-
402
- Para date pickers compactos.
403
-
404
- ```tsx
405
- import {
406
- Popover,
407
- PopoverContent,
408
- PopoverTrigger,
409
- } from "@adamosuiteservices/ui/popover";
410
- import { Button } from "@adamosuiteservices/ui/button";
411
- import { Icon } from "@adamosuiteservices/ui/icon";
412
- import { format } from "date-fns";
413
-
414
- function DatePickerPopover() {
415
- const [date, setDate] = useState<Date>();
416
-
417
- return (
418
- <Popover>
419
- <PopoverTrigger asChild>
420
- <Button variant="outline">
421
- <Icon symbol="calendar_today" />
422
- {date ? format(date, "PPP") : "Pick a date"}
423
- </Button>
424
- </PopoverTrigger>
425
- <PopoverContent className="w-auto p-0">
426
- <Calendar
427
- mode="single"
428
- selected={date}
429
- onSelect={setDate}
430
- initialFocus
431
- />
432
- </PopoverContent>
433
- </Popover>
434
- );
435
- }
436
- ```
437
-
438
- **Nota**: El background se vuelve transparente automáticamente dentro de PopoverContent.
439
-
440
- ### Con Timezone
441
-
442
- ```tsx
443
- import { useState } from "react";
444
-
445
- function TimezoneCalendar() {
446
- const [date, setDate] = useState<Date>();
447
- const [timeZone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
448
-
449
- return (
450
- <div className="space-y-2">
451
- <p className="text-sm text-muted-foreground">Timezone: {timeZone}</p>
452
- <Calendar
453
- mode="single"
454
- selected={date}
455
- onSelect={setDate}
456
- timeZone={timeZone}
457
- className="rounded-md border"
458
- />
459
- </div>
460
- );
461
- }
462
- ```
463
-
464
- ### Sin Días Externos (Outside Days)
465
-
466
- Oculta días de meses adyacentes para UI más limpia.
467
-
468
- ```tsx
469
- <Calendar
470
- mode="single"
471
- selected={date}
472
- onSelect={setDate}
473
- showOutsideDays={false}
474
- className="rounded-md border"
475
- />
476
- ```
477
-
478
- ### Con Números de Semana
479
-
480
- Útil para reportes empresariales o planificación semanal.
481
-
482
- ```tsx
483
- <Calendar
484
- mode="single"
485
- selected={date}
486
- onSelect={setDate}
487
- showWeekNumber
488
- className="rounded-md border"
489
- />
490
- ```
491
-
492
- **Formato**: Números de semana según ISO 8601 (1-53).
493
-
494
- ### Variantes de Botones de Navegación
495
-
496
- Personaliza la apariencia de los botones prev/next.
497
-
498
- ```tsx
499
- <div className="space-y-4">
500
- {/* Ghost (default) */}
501
- <Calendar mode="single" buttonVariant="ghost" className="rounded-md border" />
502
-
503
- {/* Outline */}
504
- <Calendar
505
- mode="single"
506
- buttonVariant="outline"
507
- className="rounded-md border"
508
- />
509
-
510
- {/* Secondary */}
511
- <Calendar
512
- mode="single"
513
- buttonVariant="secondary"
514
- className="rounded-md border"
515
- />
516
- </div>
517
- ```
518
-
519
- ### Rango Preseleccionado (Últimos 7 Días)
520
-
521
- ```tsx
522
- import { subDays } from "date-fns";
523
-
524
- function Last7DaysRange() {
525
- const [range, setRange] = useState<DateRange>({
526
- from: subDays(new Date(), 7),
527
- to: new Date(),
528
- });
529
-
530
- return (
531
- <Calendar
532
- mode="range"
533
- selected={range}
534
- onSelect={setRange}
535
- numberOfMonths={2}
536
- className="rounded-md border"
537
- />
538
- );
539
- }
540
- ```
541
-
542
- ### Validación de Rango Máximo
543
-
544
- Limita la cantidad de días seleccionables en un rango.
545
-
546
- ```tsx
547
- import { differenceInDays } from "date-fns";
548
-
549
- function MaxRangeCalendar() {
550
- const [range, setRange] = useState<DateRange>();
551
- const MAX_DAYS = 14;
552
-
553
- const handleSelect = (selectedRange: DateRange | undefined) => {
554
- if (!selectedRange?.from || !selectedRange?.to) {
555
- setRange(selectedRange);
556
- return;
557
- }
558
-
559
- const daysDiff = differenceInDays(selectedRange.to, selectedRange.from);
560
-
561
- if (daysDiff > MAX_DAYS) {
562
- alert(`Maximum range is ${MAX_DAYS} days`);
563
- return;
564
- }
565
-
566
- setRange(selectedRange);
567
- };
568
-
569
- return (
570
- <div className="space-y-2">
571
- <p className="text-sm text-muted-foreground">
572
- Maximum range: {MAX_DAYS} days
573
- </p>
574
- <Calendar
575
- mode="range"
576
- selected={range}
577
- onSelect={handleSelect}
578
- numberOfMonths={2}
579
- className="rounded-md border"
580
- />
581
- </div>
582
- );
583
- }
584
- ```
585
-
586
- ### Calendario con Presets de Rango
587
-
588
- Botones rápidos para rangos comunes.
589
-
590
- ```tsx
591
- import {
592
- startOfMonth,
593
- endOfMonth,
594
- subMonths,
595
- startOfWeek,
596
- endOfWeek,
597
- } from "date-fns";
598
- import { Button } from "@adamosuiteservices/ui/button";
599
-
600
- function CalendarWithPresets() {
601
- const [range, setRange] = useState<DateRange>();
602
-
603
- const presets = [
604
- {
605
- label: "Today",
606
- range: { from: new Date(), to: new Date() },
607
- },
608
- {
609
- label: "Last 7 days",
610
- range: { from: subDays(new Date(), 7), to: new Date() },
611
- },
612
- {
613
- label: "This week",
614
- range: { from: startOfWeek(new Date()), to: endOfWeek(new Date()) },
615
- },
616
- {
617
- label: "This month",
618
- range: { from: startOfMonth(new Date()), to: endOfMonth(new Date()) },
619
- },
620
- {
621
- label: "Last month",
622
- range: {
623
- from: startOfMonth(subMonths(new Date(), 1)),
624
- to: endOfMonth(subMonths(new Date(), 1)),
625
- },
626
- },
627
- ];
628
-
629
- return (
630
- <div className="flex gap-4">
631
- <div className="flex flex-col gap-2">
632
- {presets.map((preset) => (
633
- <Button
634
- key={preset.label}
635
- variant="outline"
636
- onClick={() => setRange(preset.range)}
637
- className="justify-start"
638
- >
639
- {preset.label}
640
- </Button>
641
- ))}
642
- </div>
643
- <Calendar
644
- mode="range"
645
- selected={range}
646
- onSelect={setRange}
647
- numberOfMonths={2}
648
- className="rounded-md border"
649
- />
650
- </div>
651
- );
652
- }
653
- ```
654
-
655
- ### Formateo Custom de Meses
656
-
657
- ```tsx
658
- <Calendar
659
- mode="single"
660
- captionLayout="dropdown"
661
- formatters={{
662
- formatMonthDropdown: (date) =>
663
- date.toLocaleString("es-ES", { month: "long" }),
664
- }}
665
- />
666
- ```
667
-
668
- ### Internacionalización (i18n)
669
-
670
- ```tsx
671
- import { es } from "date-fns/locale";
672
-
673
- <Calendar
674
- mode="single"
675
- locale={es}
676
- weekStartsOn={1} // Lunes
677
- selected={date}
678
- onSelect={setDate}
679
- />;
680
- ```
681
-
682
- ## Casos de Uso Comunes
683
-
684
- ### Date Picker Simple
685
-
686
- ```tsx
687
- import {
688
- Popover,
689
- PopoverContent,
690
- PopoverTrigger,
691
- } from "@adamosuiteservices/ui/popover";
692
- import { Button } from "@adamosuiteservices/ui/button";
693
- import { Icon } from "@adamosuiteservices/ui/icon";
694
- import { format } from "date-fns";
695
-
696
- <Popover>
697
- <PopoverTrigger asChild>
698
- <Button variant="outline">
699
- <Icon symbol="calendar_today" />
700
- {date ? format(date, "PPP") : "Pick a date"}
701
- </Button>
702
- </PopoverTrigger>
703
- <PopoverContent className="w-auto p-0">
704
- <Calendar mode="single" selected={date} onSelect={setDate} />
705
- </PopoverContent>
706
- </Popover>;
707
- ```
708
-
709
- ### Date Range Picker
710
-
711
- ```tsx
712
- <Popover>
713
- <PopoverTrigger asChild>
714
- <Button variant="outline">
715
- <Icon symbol="calendar_today" />
716
- {range?.from
717
- ? range.to
718
- ? `${format(range.from, "LLL dd")} - ${format(range.to, "LLL dd, y")}`
719
- : format(range.from, "LLL dd, y")
720
- : "Pick a date range"}
721
- </Button>
722
- </PopoverTrigger>
723
- <PopoverContent className="w-auto p-0">
724
- <Calendar
725
- mode="range"
726
- selected={range}
727
- onSelect={setRange}
728
- numberOfMonths={2}
729
- />
730
- </PopoverContent>
731
- </Popover>
732
- ```
733
-
734
- ### Fecha de Nacimiento
735
-
736
- ```tsx
737
- const currentYear = new Date().getFullYear();
738
-
739
- <Calendar
740
- mode="single"
741
- captionLayout="dropdown"
742
- fromYear={1950}
743
- toYear={currentYear - 18}
744
- defaultMonth={new Date(2000, 0)}
745
- selected={birthdate}
746
- onSelect={setBirthdate}
747
- />;
748
- ```
749
-
750
- ### Reserva de Hotel
751
-
752
- ```tsx
753
- <Calendar
754
- mode="range"
755
- selected={reservation}
756
- onSelect={setReservation}
757
- disabled={[
758
- // Fechas ya reservadas
759
- { from: new Date(2025, 6, 10), to: new Date(2025, 6, 15) },
760
- { from: new Date(2025, 6, 20), to: new Date(2025, 6, 25) },
761
- // No permitir pasado
762
- { before: new Date() },
763
- ]}
764
- numberOfMonths={2}
765
- className="rounded-md border"
766
- />
767
- ```
768
-
769
- ### Calendario de Disponibilidad (Solo Vista)
770
-
771
- ```tsx
772
- const bookedDates = [
773
- new Date(2025, 6, 10),
774
- new Date(2025, 6, 15),
775
- new Date(2025, 6, 20),
776
- ];
777
-
778
- <Calendar
779
- mode="default"
780
- modifiers={{
781
- booked: bookedDates,
782
- }}
783
- modifiersClassNames={{
784
- booked: "bg-destructive/10 text-destructive",
785
- }}
786
- className="rounded-md border"
787
- />;
788
- ```
789
-
790
- ### Filtro de Reportes
791
-
792
- ```tsx
793
- import { startOfMonth, endOfMonth } from "date-fns";
794
-
795
- function ReportFilter() {
796
- const [range, setRange] = useState<DateRange>({
797
- from: startOfMonth(new Date()),
798
- to: endOfMonth(new Date()),
799
- });
800
-
801
- return (
802
- <div className="space-y-4">
803
- <Calendar
804
- mode="range"
805
- selected={range}
806
- onSelect={setRange}
807
- numberOfMonths={2}
808
- className="rounded-md border"
809
- />
810
- <Button onClick={() => fetchReport(range)}>Generate Report</Button>
811
- </div>
812
- );
813
- }
814
- ```
815
-
816
- ## Mejores Prácticas
817
-
818
- ### Usa defaultMonth para Rangos
819
-
820
- ```tsx
821
- {
822
- /* ✅ Correcto - Muestra el mes de inicio del rango */
823
- }
824
- <Calendar
825
- mode="range"
826
- defaultMonth={range?.from}
827
- selected={range}
828
- onSelect={setRange}
829
- />;
830
-
831
- {
832
- /* ❌ Incorrecto - Puede mostrar mes actual sin datos */
833
- }
834
- <Calendar mode="range" selected={range} onSelect={setRange} />;
835
- ```
836
-
837
- ### Usa numberOfMonths con Rangos Largos
838
-
839
- ```tsx
840
- {
841
- /* ✅ Correcto - 2 meses para rangos */
842
- }
843
- <Calendar
844
- mode="range"
845
- numberOfMonths={2}
846
- selected={range}
847
- onSelect={setRange}
848
- />;
849
-
850
- {
851
- /* ⚠️ Aceptable - 1 mes para rangos cortos */
852
- }
853
- <Calendar mode="range" selected={range} onSelect={setRange} />;
854
- ```
855
-
856
- ### Usa captionLayout="dropdown" para Fechas Lejanas
857
-
858
- ```tsx
859
- {
860
- /* ✅ Correcto - Fecha de nacimiento con dropdowns */
861
- }
862
- <Calendar
863
- mode="single"
864
- captionLayout="dropdown"
865
- fromYear={1950}
866
- toYear={2010}
867
- defaultMonth={new Date(1990, 0)}
868
- />;
869
-
870
- {
871
- /* ❌ Incorrecto - Navegación con flechas para 30+ años */
872
- }
873
- <Calendar mode="single" captionLayout="label" fromYear={1950} />;
874
- ```
875
-
876
- ### Deshabilita Pasado para Reservas
877
-
878
- ```tsx
879
- {
880
- /* ✅ Correcto - Solo futuro */
881
- }
882
- <Calendar
883
- mode="range"
884
- disabled={{ before: new Date() }}
885
- selected={range}
886
- onSelect={setRange}
887
- />;
888
- ```
889
-
890
- ### Valida Rangos en onSelect
891
-
892
- ```tsx
893
- {
894
- /* ✅ Correcto - Valida antes de guardar */
895
- }
896
- const handleSelect = (selectedRange: DateRange | undefined) => {
897
- if (!selectedRange?.from || !selectedRange?.to) {
898
- setRange(selectedRange);
899
- return;
900
- }
901
-
902
- const daysDiff = differenceInDays(selectedRange.to, selectedRange.from);
903
-
904
- if (daysDiff > MAX_DAYS) {
905
- toast.error(`Maximum range is ${MAX_DAYS} days`);
906
- return;
907
- }
908
-
909
- setRange(selectedRange);
910
- };
911
-
912
- {
913
- /* ❌ Incorrecto - Sin validación */
914
- }
915
- <Calendar mode="range" selected={range} onSelect={setRange} />;
916
- ```
917
-
918
- ### Usa initialFocus en Popovers
919
-
920
- ```tsx
921
- {
922
- /* ✅ Correcto - Focus automático al abrir */
923
- }
924
- <PopoverContent>
925
- <Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
926
- </PopoverContent>;
927
-
928
- {
929
- /* ❌ Incorrecto - Sin initialFocus */
930
- }
931
- <PopoverContent>
932
- <Calendar mode="single" selected={date} onSelect={setDate} />
933
- </PopoverContent>;
934
- ```
935
-
936
- ### Timezone para Aplicaciones Globales
937
-
938
- ```tsx
939
- {
940
- /* ✅ Correcto - Timezone explícito */
941
- }
942
- <Calendar
943
- mode="single"
944
- timeZone="America/New_York"
945
- selected={date}
946
- onSelect={setDate}
947
- />;
948
-
949
- {
950
- /* ⚠️ Cuidado - Usa timezone del navegador por defecto */
951
- }
952
- <Calendar mode="single" selected={date} onSelect={setDate} />;
953
- ```
954
-
955
- ## Notas de Implementación
956
-
957
- - **Basado en `react-day-picker` v9+**: Librería madura y accesible
958
- - **Wrapper con mejoras**: Agrega variantes de botones, RTL support, integración con Card/Popover
959
- - **CSS Variables**: Usa `--cell-size` para tamaño de celdas (default: `--spacing(8)` = 32px)
960
- - **Auto-transparencia**: Background transparente dentro de `[data-slot=card-content]` y `[data-slot=popover-content]`
961
- - **RTL Support**: Rotación automática de chevrons con `rtl:**:[.rdp-button_next>svg]:rotate-180`
962
- - **Grid Layout**: Usa CSS Grid internamente para layout del calendario
963
- - **Focus Management**: CalendarDayButton usa `useEffect` para foco automático en día con modifier `focused`
964
- - **Data Attributes**: Días usan `data-selected-single`, `data-range-start`, `data-range-end`, `data-range-middle` para estilos específicos
965
- - **Aspect Ratio**: Días tienen `aspect-square` para celdas perfectamente cuadradas
966
- - **Button Variants**: Navegación usa `buttonVariants()` de Button component para consistencia visual
967
- - **Dropdown con Overlay**: Dropdowns usan select nativo con overlay invisible (`opacity-0`) para accesibilidad
968
- - **Responsive**: Múltiples meses cambian de `flex-row` a `flex-col` en móviles con `md:flex-row`
969
-
970
- ## Accesibilidad
971
-
972
- ### Navegación por Teclado
973
-
974
- - ✅ **Arrow keys**: Navega entre días (↑ ↓ ← →)
975
- - ✅ **PageUp/PageDown**: Navega entre meses
976
- - ✅ **Home/End**: Primer/último día del mes
977
- - ✅ **Enter/Space**: Selecciona día enfocado
978
- - ✅ **Tab**: Navega entre controles (botones prev/next, dropdowns)
979
-
980
- ### ARIA Labels
981
-
982
- ```tsx
983
- {
984
- /* ✅ Automático - react-day-picker agrega ARIA */
985
- }
986
- <Calendar mode="single" selected={date} onSelect={setDate} />;
987
- ```
988
-
989
- **Atributos automáticos**:
990
-
991
- - `role="application"` en contenedor
992
- - `aria-label` en días (ej: "Monday, October 31, 2025")
993
- - `aria-selected="true"` en días seleccionados
994
- - `aria-disabled="true"` en días deshabilitados
995
- - `aria-current="date"` en día actual
996
-
997
- ### Focus Visible
998
-
999
- Días enfocados muestran ring azul con `group-data-[focused=true]/day:ring-ring/50`.
1000
-
1001
- ### Screen Readers
1002
-
1003
- - Anuncia mes y año actual
1004
- - Anuncia días seleccionados
1005
- - Anuncia rangos ("from ... to ...")
1006
- - Anuncia días deshabilitados
1007
-
1008
- ### Labels Personalizadas
1009
-
1010
- ```tsx
1011
- <Calendar
1012
- mode="single"
1013
- aria-label="Select appointment date"
1014
- selected={date}
1015
- onSelect={setDate}
1016
- />
1017
- ```
1018
-
1019
- ## Troubleshooting
1020
-
1021
- ### Calendario No Se Ve (Invisible)
1022
-
1023
- **Problema**: El calendario se renderiza pero no es visible.
1024
-
1025
- **Solución**:
1026
-
1027
- ```tsx
1028
- // ❌ Problema - Falta CSS de react-day-picker
1029
- // Asegúrate de importar el CSS global
1030
-
1031
- // ✅ Solución - Importa en tu layout/app
1032
- import "react-day-picker/style.css";
1033
- ```
1034
-
1035
- ### Meses Múltiples No Se Alinean
1036
-
1037
- **Problema**: Con `numberOfMonths={2}`, los meses se apilan en desktop.
1038
-
1039
- **Solución**:
1040
-
1041
- ```tsx
1042
- // ✅ El componente usa md:flex-row automáticamente
1043
- // Si sigue apilando, verifica que Tailwind tenga @media queries compiladas
1044
- <Calendar numberOfMonths={2} className="rounded-md border" />
1045
- ```
1046
-
1047
- ### Dropdowns No Funcionan
1048
-
1049
- **Problema**: Los dropdowns de mes/año no responden.
1050
-
1051
- **Solución**:
1052
-
1053
- ```tsx
1054
- // ❌ Problema - Sin fromYear/toYear
1055
- <Calendar captionLayout="dropdown" />
1056
-
1057
- // ✅ Solución - Define rango de años
1058
- <Calendar
1059
- captionLayout="dropdown"
1060
- fromYear={2020}
1061
- toYear={2030}
1062
- />
1063
- ```
1064
-
1065
- ### Rango No Se Completa
1066
-
1067
- **Problema**: Al seleccionar segunda fecha, el rango se reinicia.
1068
-
1069
- **Solución**:
1070
-
1071
- ```tsx
1072
- // ✅ Verifica que el tipo sea DateRange
1073
- const [range, setRange] = useState<DateRange | undefined>();
1074
-
1075
- // ✅ Usa defaultMonth para estabilidad
1076
- <Calendar
1077
- mode="range"
1078
- defaultMonth={range?.from}
1079
- selected={range}
1080
- onSelect={setRange}
1081
- />;
1082
- ```
1083
-
1084
- ### Días Deshabilitados Siguen Seleccionables
1085
-
1086
- **Problema**: Días con `disabled` aún se pueden clickear.
1087
-
1088
- **Solución**:
1089
-
1090
- ```tsx
1091
- // ❌ Problema - Sintaxis incorrecta
1092
- <Calendar disabled={[new Date("2025-10-15")]} /> // String no funciona
1093
-
1094
- // ✅ Solución - Objetos Date válidos
1095
- <Calendar disabled={[new Date(2025, 9, 15)]} /> // Mes 9 = Octubre
1096
- ```
1097
-
1098
- ### Timezone Incorrecto
1099
-
1100
- **Problema**: Fechas se guardan en timezone diferente.
1101
-
1102
- **Solución**:
1103
-
1104
- ```tsx
1105
- // ✅ Especifica timezone explícito
1106
- <Calendar mode="single" timeZone="UTC" selected={date} onSelect={setDate} />;
1107
-
1108
- // O convierte al guardar
1109
- const handleSelect = (selectedDate: Date | undefined) => {
1110
- if (selectedDate) {
1111
- const utcDate = new Date(selectedDate.toISOString());
1112
- setDate(utcDate);
1113
- }
1114
- };
1115
- ```
1116
-
1117
- ### Popover Se Cierra al Seleccionar
1118
-
1119
- **Problema**: El popover se cierra inmediatamente al clickear un día.
1120
-
1121
- **Solución**:
1122
-
1123
- ```tsx
1124
- // ✅ Cierra manualmente después de selección
1125
- function DatePickerWithControl() {
1126
- const [open, setOpen] = useState(false);
1127
- const [date, setDate] = useState<Date>();
1128
-
1129
- return (
1130
- <Popover open={open} onOpenChange={setOpen}>
1131
- <PopoverTrigger asChild>
1132
- <Button variant="outline">
1133
- <Icon symbol="calendar_today" />
1134
- {date ? format(date, "PPP") : "Pick a date"}
1135
- </Button>
1136
- </PopoverTrigger>
1137
- <PopoverContent className="w-auto p-0">
1138
- <Calendar
1139
- mode="single"
1140
- selected={date}
1141
- onSelect={(newDate) => {
1142
- setDate(newDate);
1143
- setOpen(false); // Cierra manualmente
1144
- }}
1145
- initialFocus
1146
- />
1147
- </PopoverContent>
1148
- </Popover>
1149
- );
1150
- }
1151
- ```
1152
-
1153
- ## Referencias
1154
-
1155
- - **react-day-picker**: https://react-day-picker.js.org
1156
- - **date-fns** (formateo): https://date-fns.org
1157
- - **Radix UI Calendar** (inspiración): https://www.radix-ui.com/primitives/docs/components/calendar
1158
- - **shadcn/ui Calendar**: https://ui.shadcn.com/docs/components/calendar
1159
- - **ARIA Calendar Pattern**: https://www.w3.org/WAI/ARIA/apg/patterns/calendar/
1
+ # Calendar
2
+
3
+ Calendario interactivo altamente personalizable basado en `react-day-picker` para selección de fechas individuales, múltiples o rangos. Soporta navegación con dropdowns, múltiples meses, números de semana, timezones y fechas deshabilitadas.
4
+
5
+ ## Características Principales
6
+
7
+ - **4 modos de selección**: Single (fecha única), Multiple (múltiples fechas), Range (rango de fechas), Default (sin selección)
8
+ - **Navegación flexible**: Botones tradicionales o dropdowns mes/año para saltos rápidos
9
+ - **Múltiples meses**: Visualización simultánea de 2+ meses para rangos amplios
10
+ - **Números de semana**: Muestra semana del año (ISO 8601)
11
+ - **Fechas deshabilitadas**: Bloquea fechas específicas o rangos completos
12
+ - **Timezone awareness**: Soporte completo para zonas horarias
13
+ - **Variantes de botones**: Personaliza navegación (ghost, outline, secondary, etc.)
14
+ - **RTL support**: Rotación automática de chevrons en idiomas right-to-left
15
+ - **Integración con Card/Popover**: Background transparente automático
16
+ - **Accesibilidad**: Navegación por teclado, ARIA labels, focus visible
17
+
18
+ ## Importación
19
+
20
+ ```tsx
21
+ import { Calendar } from "@adamosuiteservices/ui/calendar";
22
+ import type { DateRange } from "react-day-picker";
23
+ ```
24
+
25
+ ```tsx
26
+ import { Calendar } from "@adamosuiteservices/ui/calendar";
27
+ import type { DateRange } from "react-day-picker";
28
+ ```
29
+
30
+ ## Uso Básico
31
+
32
+ ### Selección de Fecha Única
33
+
34
+ El modo más común para seleccionar una sola fecha.
35
+
36
+ ```tsx
37
+ import { useState } from "react";
38
+ import { Calendar } from "@adamosuiteservices/ui/calendar";
39
+
40
+ function DatePicker() {
41
+ const [date, setDate] = useState<Date | undefined>(new Date());
42
+
43
+ return (
44
+ <Calendar
45
+ mode="single"
46
+ selected={date}
47
+ onSelect={setDate}
48
+ className="rounded-md border"
49
+ />
50
+ );
51
+ }
52
+ ```
53
+
54
+ ### Navegación con Dropdowns
55
+
56
+ Para saltar rápidamente entre meses y años.
57
+
58
+ ```tsx
59
+ const [date, setDate] = useState<Date | undefined>(new Date(2025, 5, 12));
60
+
61
+ <Calendar
62
+ mode="single"
63
+ selected={date}
64
+ onSelect={setDate}
65
+ captionLayout="dropdown"
66
+ className="rounded-md border"
67
+ />;
68
+ ```
69
+
70
+ **Ventaja**: Permite seleccionar fechas lejanas sin hacer múltiples clics en flechas.
71
+
72
+ ### Rango de Fechas
73
+
74
+ Para seleccionar periodo entre dos fechas (ej: reservas de hotel).
75
+
76
+ ```tsx
77
+ import type { DateRange } from "react-day-picker";
78
+
79
+ function RangePicker() {
80
+ const [range, setRange] = useState<DateRange | undefined>({
81
+ from: new Date(2025, 5, 12),
82
+ to: new Date(2025, 5, 18),
83
+ });
84
+
85
+ return (
86
+ <Calendar
87
+ mode="range"
88
+ defaultMonth={range?.from}
89
+ selected={range}
90
+ onSelect={setRange}
91
+ className="rounded-md border"
92
+ />
93
+ );
94
+ }
95
+ ```
96
+
97
+ ### Múltiples Meses
98
+
99
+ Ideal para rangos largos o visualización amplia.
100
+
101
+ ```tsx
102
+ const [range, setRange] = useState<DateRange | undefined>({
103
+ from: new Date(2025, 5, 12),
104
+ to: new Date(2025, 6, 15),
105
+ });
106
+
107
+ <Calendar
108
+ mode="range"
109
+ defaultMonth={range?.from}
110
+ selected={range}
111
+ onSelect={setRange}
112
+ numberOfMonths={2}
113
+ className="rounded-md border"
114
+ />;
115
+ ```
116
+
117
+ **Nota**: En pantallas pequeñas, los meses se apilan verticalmente automáticamente.
118
+
119
+ ### Selección Múltiple
120
+
121
+ Para seleccionar fechas no consecutivas (ej: días de entrenamiento).
122
+
123
+ ```tsx
124
+ const [dates, setDates] = useState<Date[] | undefined>([
125
+ new Date(2025, 5, 12),
126
+ new Date(2025, 5, 15),
127
+ new Date(2025, 5, 20),
128
+ ]);
129
+
130
+ <Calendar
131
+ mode="multiple"
132
+ selected={dates}
133
+ onSelect={setDates}
134
+ className="rounded-md border"
135
+ />;
136
+ ```
137
+
138
+ ## Componentes
139
+
140
+ ### Calendar
141
+
142
+ El componente principal que renderiza el calendario completo.
143
+
144
+ #### Props
145
+
146
+ | Prop | Tipo | Default | Descripción |
147
+ | ----------------- | ---------------------------------------------------------------- | ------------- | ------------------------------------------- |
148
+ | `mode` | `"single" \| "multiple" \| "range" \| "default"` | `"default"` | Modo de selección de fechas |
149
+ | `selected` | `Date \| Date[] \| DateRange \| undefined` | `undefined` | Fecha(s) seleccionada(s) según modo |
150
+ | `onSelect` | `(date) => void` | - | Callback cuando cambia la selección |
151
+ | `defaultMonth` | `Date` | Mes actual | Mes inicial a mostrar |
152
+ | `numberOfMonths` | `number` | `1` | Cantidad de meses a mostrar simultáneamente |
153
+ | `captionLayout` | `"label" \| "dropdown" \| "dropdown-months" \| "dropdown-years"` | `"label"` | Tipo de navegación del header |
154
+ | `showOutsideDays` | `boolean` | `true` | Mostrar días de meses adyacentes |
155
+ | `showWeekNumber` | `boolean` | `false` | Mostrar números de semana |
156
+ | `disabled` | `Date \| Date[] \| DateRange \| ((date: Date) => boolean)` | - | Fechas deshabilitadas |
157
+ | `hidden` | `Date \| Date[] \| DateRange \| ((date: Date) => boolean)` | - | Fechas ocultas completamente |
158
+ | `fromDate` | `Date` | - | Fecha mínima seleccionable |
159
+ | `toDate` | `Date` | - | Fecha máxima seleccionable |
160
+ | `fromMonth` | `Date` | - | Mes mínimo navegable |
161
+ | `toMonth` | `Date` | - | Mes máximo navegable |
162
+ | `fromYear` | `number` | - | Año mínimo navegable |
163
+ | `toYear` | `number` | - | Año máximo navegable |
164
+ | `timeZone` | `string` | Sistema | Zona horaria (ej: `"America/New_York"`) |
165
+ | `locale` | `Locale` | Sistema | Objeto Locale de `date-fns` para i18n |
166
+ | `weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `0` | Día inicio de semana (0=Domingo, 1=Lunes) |
167
+ | `buttonVariant` | `ButtonProps["variant"]` | `"secondary"` | Variante para botones de navegación |
168
+ | `className` | `string` | - | Clases CSS adicionales para el contenedor |
169
+ | `classNames` | `ClassNames` | - | Objeto con clases para elementos internos |
170
+ | `components` | `Components` | - | Componentes custom para override |
171
+ | `formatters` | `Formatters` | - | Funciones de formateo custom |
172
+
173
+ ### CalendarDayButton
174
+
175
+ Componente interno que renderiza cada día del calendario. Raramente se usa directamente.
176
+
177
+ ## Modos de Selección
178
+
179
+ ### Mode: "single"
180
+
181
+ Un solo día seleccionado a la vez.
182
+
183
+ ```tsx
184
+ // Tipo de selected
185
+ selected: Date | undefined
186
+
187
+ // Callback
188
+ onSelect: (date: Date | undefined) => void
189
+ ```
190
+
191
+ **Uso común**: Fecha de nacimiento, vencimiento de documento, fecha límite.
192
+
193
+ ### Mode: "multiple"
194
+
195
+ Array de días no consecutivos.
196
+
197
+ ```tsx
198
+ // Tipo de selected
199
+ selected: Date[] | undefined
200
+
201
+ // Callback
202
+ onSelect: (dates: Date[] | undefined) => void
203
+ ```
204
+
205
+ **Uso común**: Días de entrenamiento, fechas de eventos recurrentes, días festivos.
206
+
207
+ ### Mode: "range"
208
+
209
+ Rango continuo entre dos fechas.
210
+
211
+ ```tsx
212
+ import type { DateRange } from "react-day-picker";
213
+
214
+ // Tipo de selected
215
+ selected: DateRange | undefined // { from?: Date; to?: Date }
216
+
217
+ // Callback
218
+ onSelect: (range: DateRange | undefined) => void
219
+ ```
220
+
221
+ **Uso común**: Reservas de hotel, filtros de reportes, periodos de vacaciones.
222
+
223
+ ### Mode: "default"
224
+
225
+ Sin selección (solo visualización).
226
+
227
+ ```tsx
228
+ <Calendar mode="default" />
229
+ ```
230
+
231
+ **Uso común**: Mostrar calendario estático, vista de disponibilidad sin interacción.
232
+
233
+ ## Captionlayout (Navegación)
234
+
235
+ ### "label" (Default)
236
+
237
+ Navegación tradicional con flechas.
238
+
239
+ ```tsx
240
+ <Calendar captionLayout="label" />
241
+ ```
242
+
243
+ **Ventaja**: UI simple, menos espacio vertical.
244
+
245
+ ### "dropdown"
246
+
247
+ Dropdowns para mes Y año.
248
+
249
+ ```tsx
250
+ <Calendar captionLayout="dropdown" fromYear={2020} toYear={2030} />
251
+ ```
252
+
253
+ **Ventaja**: Saltos rápidos a fechas lejanas. Ideal para fecha de nacimiento.
254
+
255
+ ### "dropdown-months"
256
+
257
+ Solo dropdown de meses, año con flechas.
258
+
259
+ ```tsx
260
+ <Calendar captionLayout="dropdown-months" />
261
+ ```
262
+
263
+ ### "dropdown-years"
264
+
265
+ Solo dropdown de años, mes con flechas.
266
+
267
+ ```tsx
268
+ <Calendar captionLayout="dropdown-years" />
269
+ ```
270
+
271
+ ## Patrones Avanzados
272
+
273
+ ### Fechas Deshabilitadas (Múltiples Formas)
274
+
275
+ #### Fechas Específicas
276
+
277
+ ```tsx
278
+ const disabledDays = [
279
+ new Date(2025, 9, 15),
280
+ new Date(2025, 9, 16),
281
+ new Date(2025, 9, 17),
282
+ ];
283
+
284
+ <Calendar
285
+ mode="single"
286
+ disabled={disabledDays}
287
+ selected={date}
288
+ onSelect={setDate}
289
+ />;
290
+ ```
291
+
292
+ #### Rangos de Fechas
293
+
294
+ ```tsx
295
+ const disabledRanges = [
296
+ { from: new Date(2025, 9, 20), to: new Date(2025, 9, 25) },
297
+ { from: new Date(2025, 10, 1), to: new Date(2025, 10, 7) },
298
+ ];
299
+
300
+ <Calendar disabled={disabledRanges} />;
301
+ ```
302
+
303
+ #### Función Dinámica
304
+
305
+ ```tsx
306
+ // Deshabilitar fines de semana
307
+ const isWeekend = (date: Date) => {
308
+ const day = date.getDay();
309
+ return day === 0 || day === 6;
310
+ };
311
+
312
+ <Calendar disabled={isWeekend} />;
313
+ ```
314
+
315
+ #### Deshabilitar Pasado
316
+
317
+ ```tsx
318
+ // Solo fechas futuras
319
+ <Calendar
320
+ disabled={{ before: new Date() }}
321
+ mode="single"
322
+ selected={date}
323
+ onSelect={setDate}
324
+ />
325
+ ```
326
+
327
+ #### Deshabilitar Futuro
328
+
329
+ ```tsx
330
+ // Solo fechas pasadas
331
+ <Calendar
332
+ disabled={{ after: new Date() }}
333
+ mode="single"
334
+ selected={date}
335
+ onSelect={setDate}
336
+ />
337
+ ```
338
+
339
+ ### Rango de Fechas Permitidas
340
+
341
+ #### Con fromDate y toDate
342
+
343
+ ```tsx
344
+ // Solo permitir 30 días hacia adelante
345
+ <Calendar
346
+ mode="single"
347
+ fromDate={new Date()}
348
+ toDate={new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)}
349
+ selected={date}
350
+ onSelect={setDate}
351
+ />
352
+ ```
353
+
354
+ #### Con fromYear y toYear
355
+
356
+ ```tsx
357
+ // Para fecha de nacimiento (mayores de 18)
358
+ const currentYear = new Date().getFullYear();
359
+
360
+ <Calendar
361
+ mode="single"
362
+ captionLayout="dropdown"
363
+ fromYear={1950}
364
+ toYear={currentYear - 18}
365
+ defaultMonth={new Date(2000, 0)}
366
+ selected={birthdate}
367
+ onSelect={setBirthdate}
368
+ />;
369
+ ```
370
+
371
+ ### Calendario en Card
372
+
373
+ Integración perfecta con componente Card (background transparente automático).
374
+
375
+ ```tsx
376
+ import {
377
+ Card,
378
+ CardContent,
379
+ CardDescription,
380
+ CardHeader,
381
+ CardTitle,
382
+ } from "@adamosuiteservices/ui/card";
383
+
384
+ <Card className="w-fit">
385
+ <CardHeader>
386
+ <CardTitle>Select Date</CardTitle>
387
+ <CardDescription>Choose a date for your appointment</CardDescription>
388
+ </CardHeader>
389
+ <CardContent>
390
+ <Calendar
391
+ mode="single"
392
+ selected={date}
393
+ onSelect={setDate}
394
+ captionLayout="dropdown"
395
+ />
396
+ </CardContent>
397
+ </Card>;
398
+ ```
399
+
400
+ ### Calendario en Popover
401
+
402
+ Para date pickers compactos.
403
+
404
+ ```tsx
405
+ import {
406
+ Popover,
407
+ PopoverContent,
408
+ PopoverTrigger,
409
+ } from "@adamosuiteservices/ui/popover";
410
+ import { Button } from "@adamosuiteservices/ui/button";
411
+ import { Icon } from "@adamosuiteservices/ui/icon";
412
+ import { format } from "date-fns";
413
+
414
+ function DatePickerPopover() {
415
+ const [date, setDate] = useState<Date>();
416
+
417
+ return (
418
+ <Popover>
419
+ <PopoverTrigger asChild>
420
+ <Button variant="outline">
421
+ <Icon symbol="calendar_today" />
422
+ {date ? format(date, "PPP") : "Pick a date"}
423
+ </Button>
424
+ </PopoverTrigger>
425
+ <PopoverContent className="w-auto p-0">
426
+ <Calendar
427
+ mode="single"
428
+ selected={date}
429
+ onSelect={setDate}
430
+ initialFocus
431
+ />
432
+ </PopoverContent>
433
+ </Popover>
434
+ );
435
+ }
436
+ ```
437
+
438
+ **Nota**: El background se vuelve transparente automáticamente dentro de PopoverContent.
439
+
440
+ ### Con Timezone
441
+
442
+ ```tsx
443
+ import { useState } from "react";
444
+
445
+ function TimezoneCalendar() {
446
+ const [date, setDate] = useState<Date>();
447
+ const [timeZone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
448
+
449
+ return (
450
+ <div className="space-y-2">
451
+ <p className="text-sm text-muted-foreground">Timezone: {timeZone}</p>
452
+ <Calendar
453
+ mode="single"
454
+ selected={date}
455
+ onSelect={setDate}
456
+ timeZone={timeZone}
457
+ className="rounded-md border"
458
+ />
459
+ </div>
460
+ );
461
+ }
462
+ ```
463
+
464
+ ### Sin Días Externos (Outside Days)
465
+
466
+ Oculta días de meses adyacentes para UI más limpia.
467
+
468
+ ```tsx
469
+ <Calendar
470
+ mode="single"
471
+ selected={date}
472
+ onSelect={setDate}
473
+ showOutsideDays={false}
474
+ className="rounded-md border"
475
+ />
476
+ ```
477
+
478
+ ### Con Números de Semana
479
+
480
+ Útil para reportes empresariales o planificación semanal.
481
+
482
+ ```tsx
483
+ <Calendar
484
+ mode="single"
485
+ selected={date}
486
+ onSelect={setDate}
487
+ showWeekNumber
488
+ className="rounded-md border"
489
+ />
490
+ ```
491
+
492
+ **Formato**: Números de semana según ISO 8601 (1-53).
493
+
494
+ ### Variantes de Botones de Navegación
495
+
496
+ Personaliza la apariencia de los botones prev/next.
497
+
498
+ ```tsx
499
+ <div className="space-y-4">
500
+ {/* Ghost (default) */}
501
+ <Calendar mode="single" buttonVariant="ghost" className="rounded-md border" />
502
+
503
+ {/* Outline */}
504
+ <Calendar
505
+ mode="single"
506
+ buttonVariant="outline"
507
+ className="rounded-md border"
508
+ />
509
+
510
+ {/* Secondary */}
511
+ <Calendar
512
+ mode="single"
513
+ buttonVariant="secondary"
514
+ className="rounded-md border"
515
+ />
516
+ </div>
517
+ ```
518
+
519
+ ### Rango Preseleccionado (Últimos 7 Días)
520
+
521
+ ```tsx
522
+ import { subDays } from "date-fns";
523
+
524
+ function Last7DaysRange() {
525
+ const [range, setRange] = useState<DateRange>({
526
+ from: subDays(new Date(), 7),
527
+ to: new Date(),
528
+ });
529
+
530
+ return (
531
+ <Calendar
532
+ mode="range"
533
+ selected={range}
534
+ onSelect={setRange}
535
+ numberOfMonths={2}
536
+ className="rounded-md border"
537
+ />
538
+ );
539
+ }
540
+ ```
541
+
542
+ ### Validación de Rango Máximo
543
+
544
+ Limita la cantidad de días seleccionables en un rango.
545
+
546
+ ```tsx
547
+ import { differenceInDays } from "date-fns";
548
+
549
+ function MaxRangeCalendar() {
550
+ const [range, setRange] = useState<DateRange>();
551
+ const MAX_DAYS = 14;
552
+
553
+ const handleSelect = (selectedRange: DateRange | undefined) => {
554
+ if (!selectedRange?.from || !selectedRange?.to) {
555
+ setRange(selectedRange);
556
+ return;
557
+ }
558
+
559
+ const daysDiff = differenceInDays(selectedRange.to, selectedRange.from);
560
+
561
+ if (daysDiff > MAX_DAYS) {
562
+ alert(`Maximum range is ${MAX_DAYS} days`);
563
+ return;
564
+ }
565
+
566
+ setRange(selectedRange);
567
+ };
568
+
569
+ return (
570
+ <div className="space-y-2">
571
+ <p className="text-sm text-muted-foreground">
572
+ Maximum range: {MAX_DAYS} days
573
+ </p>
574
+ <Calendar
575
+ mode="range"
576
+ selected={range}
577
+ onSelect={handleSelect}
578
+ numberOfMonths={2}
579
+ className="rounded-md border"
580
+ />
581
+ </div>
582
+ );
583
+ }
584
+ ```
585
+
586
+ ### Calendario con Presets de Rango
587
+
588
+ Botones rápidos para rangos comunes.
589
+
590
+ ```tsx
591
+ import {
592
+ startOfMonth,
593
+ endOfMonth,
594
+ subMonths,
595
+ startOfWeek,
596
+ endOfWeek,
597
+ } from "date-fns";
598
+ import { Button } from "@adamosuiteservices/ui/button";
599
+
600
+ function CalendarWithPresets() {
601
+ const [range, setRange] = useState<DateRange>();
602
+
603
+ const presets = [
604
+ {
605
+ label: "Today",
606
+ range: { from: new Date(), to: new Date() },
607
+ },
608
+ {
609
+ label: "Last 7 days",
610
+ range: { from: subDays(new Date(), 7), to: new Date() },
611
+ },
612
+ {
613
+ label: "This week",
614
+ range: { from: startOfWeek(new Date()), to: endOfWeek(new Date()) },
615
+ },
616
+ {
617
+ label: "This month",
618
+ range: { from: startOfMonth(new Date()), to: endOfMonth(new Date()) },
619
+ },
620
+ {
621
+ label: "Last month",
622
+ range: {
623
+ from: startOfMonth(subMonths(new Date(), 1)),
624
+ to: endOfMonth(subMonths(new Date(), 1)),
625
+ },
626
+ },
627
+ ];
628
+
629
+ return (
630
+ <div className="flex gap-4">
631
+ <div className="flex flex-col gap-2">
632
+ {presets.map((preset) => (
633
+ <Button
634
+ key={preset.label}
635
+ variant="outline"
636
+ onClick={() => setRange(preset.range)}
637
+ className="justify-start"
638
+ >
639
+ {preset.label}
640
+ </Button>
641
+ ))}
642
+ </div>
643
+ <Calendar
644
+ mode="range"
645
+ selected={range}
646
+ onSelect={setRange}
647
+ numberOfMonths={2}
648
+ className="rounded-md border"
649
+ />
650
+ </div>
651
+ );
652
+ }
653
+ ```
654
+
655
+ ### Formateo Custom de Meses
656
+
657
+ ```tsx
658
+ <Calendar
659
+ mode="single"
660
+ captionLayout="dropdown"
661
+ formatters={{
662
+ formatMonthDropdown: (date) =>
663
+ date.toLocaleString("es-ES", { month: "long" }),
664
+ }}
665
+ />
666
+ ```
667
+
668
+ ### Internacionalización (i18n)
669
+
670
+ ```tsx
671
+ import { es } from "date-fns/locale";
672
+
673
+ <Calendar
674
+ mode="single"
675
+ locale={es}
676
+ weekStartsOn={1} // Lunes
677
+ selected={date}
678
+ onSelect={setDate}
679
+ />;
680
+ ```
681
+
682
+ ## Casos de Uso Comunes
683
+
684
+ ### Date Picker Simple
685
+
686
+ ```tsx
687
+ import {
688
+ Popover,
689
+ PopoverContent,
690
+ PopoverTrigger,
691
+ } from "@adamosuiteservices/ui/popover";
692
+ import { Button } from "@adamosuiteservices/ui/button";
693
+ import { Icon } from "@adamosuiteservices/ui/icon";
694
+ import { format } from "date-fns";
695
+
696
+ <Popover>
697
+ <PopoverTrigger asChild>
698
+ <Button variant="outline">
699
+ <Icon symbol="calendar_today" />
700
+ {date ? format(date, "PPP") : "Pick a date"}
701
+ </Button>
702
+ </PopoverTrigger>
703
+ <PopoverContent className="w-auto p-0">
704
+ <Calendar mode="single" selected={date} onSelect={setDate} />
705
+ </PopoverContent>
706
+ </Popover>;
707
+ ```
708
+
709
+ ### Date Range Picker
710
+
711
+ ```tsx
712
+ <Popover>
713
+ <PopoverTrigger asChild>
714
+ <Button variant="outline">
715
+ <Icon symbol="calendar_today" />
716
+ {range?.from
717
+ ? range.to
718
+ ? `${format(range.from, "LLL dd")} - ${format(range.to, "LLL dd, y")}`
719
+ : format(range.from, "LLL dd, y")
720
+ : "Pick a date range"}
721
+ </Button>
722
+ </PopoverTrigger>
723
+ <PopoverContent className="w-auto p-0">
724
+ <Calendar
725
+ mode="range"
726
+ selected={range}
727
+ onSelect={setRange}
728
+ numberOfMonths={2}
729
+ />
730
+ </PopoverContent>
731
+ </Popover>
732
+ ```
733
+
734
+ ### Fecha de Nacimiento
735
+
736
+ ```tsx
737
+ const currentYear = new Date().getFullYear();
738
+
739
+ <Calendar
740
+ mode="single"
741
+ captionLayout="dropdown"
742
+ fromYear={1950}
743
+ toYear={currentYear - 18}
744
+ defaultMonth={new Date(2000, 0)}
745
+ selected={birthdate}
746
+ onSelect={setBirthdate}
747
+ />;
748
+ ```
749
+
750
+ ### Reserva de Hotel
751
+
752
+ ```tsx
753
+ <Calendar
754
+ mode="range"
755
+ selected={reservation}
756
+ onSelect={setReservation}
757
+ disabled={[
758
+ // Fechas ya reservadas
759
+ { from: new Date(2025, 6, 10), to: new Date(2025, 6, 15) },
760
+ { from: new Date(2025, 6, 20), to: new Date(2025, 6, 25) },
761
+ // No permitir pasado
762
+ { before: new Date() },
763
+ ]}
764
+ numberOfMonths={2}
765
+ className="rounded-md border"
766
+ />
767
+ ```
768
+
769
+ ### Calendario de Disponibilidad (Solo Vista)
770
+
771
+ ```tsx
772
+ const bookedDates = [
773
+ new Date(2025, 6, 10),
774
+ new Date(2025, 6, 15),
775
+ new Date(2025, 6, 20),
776
+ ];
777
+
778
+ <Calendar
779
+ mode="default"
780
+ modifiers={{
781
+ booked: bookedDates,
782
+ }}
783
+ modifiersClassNames={{
784
+ booked: "bg-destructive/10 text-destructive",
785
+ }}
786
+ className="rounded-md border"
787
+ />;
788
+ ```
789
+
790
+ ### Filtro de Reportes
791
+
792
+ ```tsx
793
+ import { startOfMonth, endOfMonth } from "date-fns";
794
+
795
+ function ReportFilter() {
796
+ const [range, setRange] = useState<DateRange>({
797
+ from: startOfMonth(new Date()),
798
+ to: endOfMonth(new Date()),
799
+ });
800
+
801
+ return (
802
+ <div className="space-y-4">
803
+ <Calendar
804
+ mode="range"
805
+ selected={range}
806
+ onSelect={setRange}
807
+ numberOfMonths={2}
808
+ className="rounded-md border"
809
+ />
810
+ <Button onClick={() => fetchReport(range)}>Generate Report</Button>
811
+ </div>
812
+ );
813
+ }
814
+ ```
815
+
816
+ ## Mejores Prácticas
817
+
818
+ ### Usa defaultMonth para Rangos
819
+
820
+ ```tsx
821
+ {
822
+ /* ✅ Correcto - Muestra el mes de inicio del rango */
823
+ }
824
+ <Calendar
825
+ mode="range"
826
+ defaultMonth={range?.from}
827
+ selected={range}
828
+ onSelect={setRange}
829
+ />;
830
+
831
+ {
832
+ /* ❌ Incorrecto - Puede mostrar mes actual sin datos */
833
+ }
834
+ <Calendar mode="range" selected={range} onSelect={setRange} />;
835
+ ```
836
+
837
+ ### Usa numberOfMonths con Rangos Largos
838
+
839
+ ```tsx
840
+ {
841
+ /* ✅ Correcto - 2 meses para rangos */
842
+ }
843
+ <Calendar
844
+ mode="range"
845
+ numberOfMonths={2}
846
+ selected={range}
847
+ onSelect={setRange}
848
+ />;
849
+
850
+ {
851
+ /* ⚠️ Aceptable - 1 mes para rangos cortos */
852
+ }
853
+ <Calendar mode="range" selected={range} onSelect={setRange} />;
854
+ ```
855
+
856
+ ### Usa captionLayout="dropdown" para Fechas Lejanas
857
+
858
+ ```tsx
859
+ {
860
+ /* ✅ Correcto - Fecha de nacimiento con dropdowns */
861
+ }
862
+ <Calendar
863
+ mode="single"
864
+ captionLayout="dropdown"
865
+ fromYear={1950}
866
+ toYear={2010}
867
+ defaultMonth={new Date(1990, 0)}
868
+ />;
869
+
870
+ {
871
+ /* ❌ Incorrecto - Navegación con flechas para 30+ años */
872
+ }
873
+ <Calendar mode="single" captionLayout="label" fromYear={1950} />;
874
+ ```
875
+
876
+ ### Deshabilita Pasado para Reservas
877
+
878
+ ```tsx
879
+ {
880
+ /* ✅ Correcto - Solo futuro */
881
+ }
882
+ <Calendar
883
+ mode="range"
884
+ disabled={{ before: new Date() }}
885
+ selected={range}
886
+ onSelect={setRange}
887
+ />;
888
+ ```
889
+
890
+ ### Valida Rangos en onSelect
891
+
892
+ ```tsx
893
+ {
894
+ /* ✅ Correcto - Valida antes de guardar */
895
+ }
896
+ const handleSelect = (selectedRange: DateRange | undefined) => {
897
+ if (!selectedRange?.from || !selectedRange?.to) {
898
+ setRange(selectedRange);
899
+ return;
900
+ }
901
+
902
+ const daysDiff = differenceInDays(selectedRange.to, selectedRange.from);
903
+
904
+ if (daysDiff > MAX_DAYS) {
905
+ toast.error(`Maximum range is ${MAX_DAYS} days`);
906
+ return;
907
+ }
908
+
909
+ setRange(selectedRange);
910
+ };
911
+
912
+ {
913
+ /* ❌ Incorrecto - Sin validación */
914
+ }
915
+ <Calendar mode="range" selected={range} onSelect={setRange} />;
916
+ ```
917
+
918
+ ### Usa initialFocus en Popovers
919
+
920
+ ```tsx
921
+ {
922
+ /* ✅ Correcto - Focus automático al abrir */
923
+ }
924
+ <PopoverContent>
925
+ <Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
926
+ </PopoverContent>;
927
+
928
+ {
929
+ /* ❌ Incorrecto - Sin initialFocus */
930
+ }
931
+ <PopoverContent>
932
+ <Calendar mode="single" selected={date} onSelect={setDate} />
933
+ </PopoverContent>;
934
+ ```
935
+
936
+ ### Timezone para Aplicaciones Globales
937
+
938
+ ```tsx
939
+ {
940
+ /* ✅ Correcto - Timezone explícito */
941
+ }
942
+ <Calendar
943
+ mode="single"
944
+ timeZone="America/New_York"
945
+ selected={date}
946
+ onSelect={setDate}
947
+ />;
948
+
949
+ {
950
+ /* ⚠️ Cuidado - Usa timezone del navegador por defecto */
951
+ }
952
+ <Calendar mode="single" selected={date} onSelect={setDate} />;
953
+ ```
954
+
955
+ ## Notas de Implementación
956
+
957
+ - **Basado en `react-day-picker` v9+**: Librería madura y accesible
958
+ - **Wrapper con mejoras**: Agrega variantes de botones, RTL support, integración con Card/Popover
959
+ - **CSS Variables**: Usa `--cell-size` para tamaño de celdas (default: `--spacing(8)` = 32px)
960
+ - **Auto-transparencia**: Background transparente dentro de `[data-slot=card-content]` y `[data-slot=popover-content]`
961
+ - **RTL Support**: Rotación automática de chevrons con `rtl:**:[.rdp-button_next>svg]:rotate-180`
962
+ - **Grid Layout**: Usa CSS Grid internamente para layout del calendario
963
+ - **Focus Management**: CalendarDayButton usa `useEffect` para foco automático en día con modifier `focused`
964
+ - **Data Attributes**: Días usan `data-selected-single`, `data-range-start`, `data-range-end`, `data-range-middle` para estilos específicos
965
+ - **Aspect Ratio**: Días tienen `aspect-square` para celdas perfectamente cuadradas
966
+ - **Button Variants**: Navegación usa `buttonVariants()` de Button component para consistencia visual
967
+ - **Dropdown con Overlay**: Dropdowns usan select nativo con overlay invisible (`opacity-0`) para accesibilidad
968
+ - **Responsive**: Múltiples meses cambian de `flex-row` a `flex-col` en móviles con `md:flex-row`
969
+
970
+ ## Accesibilidad
971
+
972
+ ### Navegación por Teclado
973
+
974
+ - ✅ **Arrow keys**: Navega entre días (↑ ↓ ← →)
975
+ - ✅ **PageUp/PageDown**: Navega entre meses
976
+ - ✅ **Home/End**: Primer/último día del mes
977
+ - ✅ **Enter/Space**: Selecciona día enfocado
978
+ - ✅ **Tab**: Navega entre controles (botones prev/next, dropdowns)
979
+
980
+ ### ARIA Labels
981
+
982
+ ```tsx
983
+ {
984
+ /* ✅ Automático - react-day-picker agrega ARIA */
985
+ }
986
+ <Calendar mode="single" selected={date} onSelect={setDate} />;
987
+ ```
988
+
989
+ **Atributos automáticos**:
990
+
991
+ - `role="application"` en contenedor
992
+ - `aria-label` en días (ej: "Monday, October 31, 2025")
993
+ - `aria-selected="true"` en días seleccionados
994
+ - `aria-disabled="true"` en días deshabilitados
995
+ - `aria-current="date"` en día actual
996
+
997
+ ### Focus Visible
998
+
999
+ Días enfocados muestran ring azul con `group-data-[focused=true]/day:ring-ring/50`.
1000
+
1001
+ ### Screen Readers
1002
+
1003
+ - Anuncia mes y año actual
1004
+ - Anuncia días seleccionados
1005
+ - Anuncia rangos ("from ... to ...")
1006
+ - Anuncia días deshabilitados
1007
+
1008
+ ### Labels Personalizadas
1009
+
1010
+ ```tsx
1011
+ <Calendar
1012
+ mode="single"
1013
+ aria-label="Select appointment date"
1014
+ selected={date}
1015
+ onSelect={setDate}
1016
+ />
1017
+ ```
1018
+
1019
+ ## Troubleshooting
1020
+
1021
+ ### Calendario No Se Ve (Invisible)
1022
+
1023
+ **Problema**: El calendario se renderiza pero no es visible.
1024
+
1025
+ **Solución**:
1026
+
1027
+ ```tsx
1028
+ // ❌ Problema - Falta CSS de react-day-picker
1029
+ // Asegúrate de importar el CSS global
1030
+
1031
+ // ✅ Solución - Importa en tu layout/app
1032
+ import "react-day-picker/style.css";
1033
+ ```
1034
+
1035
+ ### Meses Múltiples No Se Alinean
1036
+
1037
+ **Problema**: Con `numberOfMonths={2}`, los meses se apilan en desktop.
1038
+
1039
+ **Solución**:
1040
+
1041
+ ```tsx
1042
+ // ✅ El componente usa md:flex-row automáticamente
1043
+ // Si sigue apilando, verifica que Tailwind tenga @media queries compiladas
1044
+ <Calendar numberOfMonths={2} className="rounded-md border" />
1045
+ ```
1046
+
1047
+ ### Dropdowns No Funcionan
1048
+
1049
+ **Problema**: Los dropdowns de mes/año no responden.
1050
+
1051
+ **Solución**:
1052
+
1053
+ ```tsx
1054
+ // ❌ Problema - Sin fromYear/toYear
1055
+ <Calendar captionLayout="dropdown" />
1056
+
1057
+ // ✅ Solución - Define rango de años
1058
+ <Calendar
1059
+ captionLayout="dropdown"
1060
+ fromYear={2020}
1061
+ toYear={2030}
1062
+ />
1063
+ ```
1064
+
1065
+ ### Rango No Se Completa
1066
+
1067
+ **Problema**: Al seleccionar segunda fecha, el rango se reinicia.
1068
+
1069
+ **Solución**:
1070
+
1071
+ ```tsx
1072
+ // ✅ Verifica que el tipo sea DateRange
1073
+ const [range, setRange] = useState<DateRange | undefined>();
1074
+
1075
+ // ✅ Usa defaultMonth para estabilidad
1076
+ <Calendar
1077
+ mode="range"
1078
+ defaultMonth={range?.from}
1079
+ selected={range}
1080
+ onSelect={setRange}
1081
+ />;
1082
+ ```
1083
+
1084
+ ### Días Deshabilitados Siguen Seleccionables
1085
+
1086
+ **Problema**: Días con `disabled` aún se pueden clickear.
1087
+
1088
+ **Solución**:
1089
+
1090
+ ```tsx
1091
+ // ❌ Problema - Sintaxis incorrecta
1092
+ <Calendar disabled={[new Date("2025-10-15")]} /> // String no funciona
1093
+
1094
+ // ✅ Solución - Objetos Date válidos
1095
+ <Calendar disabled={[new Date(2025, 9, 15)]} /> // Mes 9 = Octubre
1096
+ ```
1097
+
1098
+ ### Timezone Incorrecto
1099
+
1100
+ **Problema**: Fechas se guardan en timezone diferente.
1101
+
1102
+ **Solución**:
1103
+
1104
+ ```tsx
1105
+ // ✅ Especifica timezone explícito
1106
+ <Calendar mode="single" timeZone="UTC" selected={date} onSelect={setDate} />;
1107
+
1108
+ // O convierte al guardar
1109
+ const handleSelect = (selectedDate: Date | undefined) => {
1110
+ if (selectedDate) {
1111
+ const utcDate = new Date(selectedDate.toISOString());
1112
+ setDate(utcDate);
1113
+ }
1114
+ };
1115
+ ```
1116
+
1117
+ ### Popover Se Cierra al Seleccionar
1118
+
1119
+ **Problema**: El popover se cierra inmediatamente al clickear un día.
1120
+
1121
+ **Solución**:
1122
+
1123
+ ```tsx
1124
+ // ✅ Cierra manualmente después de selección
1125
+ function DatePickerWithControl() {
1126
+ const [open, setOpen] = useState(false);
1127
+ const [date, setDate] = useState<Date>();
1128
+
1129
+ return (
1130
+ <Popover open={open} onOpenChange={setOpen}>
1131
+ <PopoverTrigger asChild>
1132
+ <Button variant="outline">
1133
+ <Icon symbol="calendar_today" />
1134
+ {date ? format(date, "PPP") : "Pick a date"}
1135
+ </Button>
1136
+ </PopoverTrigger>
1137
+ <PopoverContent className="w-auto p-0">
1138
+ <Calendar
1139
+ mode="single"
1140
+ selected={date}
1141
+ onSelect={(newDate) => {
1142
+ setDate(newDate);
1143
+ setOpen(false); // Cierra manualmente
1144
+ }}
1145
+ initialFocus
1146
+ />
1147
+ </PopoverContent>
1148
+ </Popover>
1149
+ );
1150
+ }
1151
+ ```
1152
+
1153
+ ## Referencias
1154
+
1155
+ - **react-day-picker**: https://react-day-picker.js.org
1156
+ - **date-fns** (formateo): https://date-fns.org
1157
+ - **Radix UI Calendar** (inspiración): https://www.radix-ui.com/primitives/docs/components/calendar
1158
+ - **shadcn/ui Calendar**: https://ui.shadcn.com/docs/components/calendar
1159
+ - **ARIA Calendar Pattern**: https://www.w3.org/WAI/ARIA/apg/patterns/calendar/