@axzydev/axzy_ui_system 1.2.1 → 1.2.3

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 (207) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +82 -1
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/App.tsx +354 -0
  9. package/src/assets/logo.png +0 -0
  10. package/src/assets/react.svg +1 -0
  11. package/src/components/alert/alert.props.ts +13 -0
  12. package/src/components/alert/alert.stories.tsx +41 -0
  13. package/src/components/alert/alert.tsx +53 -0
  14. package/src/components/avatar/avatar.props.ts +14 -0
  15. package/src/components/avatar/avatar.stories.tsx +46 -0
  16. package/src/components/avatar/avatar.tsx +53 -0
  17. package/src/components/badget/badget.props.ts +12 -0
  18. package/src/components/badget/badget.stories.tsx +76 -0
  19. package/src/components/badget/badget.tsx +61 -0
  20. package/src/components/breadcrumbs/breadcrumbs.props.ts +13 -0
  21. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +21 -0
  22. package/src/components/breadcrumbs/breadcrumbs.tsx +34 -0
  23. package/src/components/button/button.props.ts +18 -0
  24. package/src/components/button/button.stories.tsx +174 -0
  25. package/src/components/button/button.tsx +117 -0
  26. package/src/components/calendar/calendar.props.ts +33 -0
  27. package/src/components/calendar/calendar.stories.tsx +91 -0
  28. package/src/components/calendar/calendar.tsx +608 -0
  29. package/src/components/calendar/index.ts +3 -0
  30. package/src/components/card/card.props.ts +13 -0
  31. package/src/components/card/card.stories.tsx +58 -0
  32. package/src/components/card/card.tsx +79 -0
  33. package/src/components/checkbox/checkbox.props.ts +11 -0
  34. package/src/components/checkbox/checkbox.stories.tsx +54 -0
  35. package/src/components/checkbox/checkbox.tsx +52 -0
  36. package/src/components/confirm-dialog/confirm-dialog.props.ts +14 -0
  37. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +33 -0
  38. package/src/components/confirm-dialog/confirm-dialog.tsx +45 -0
  39. package/src/components/data-table/ITDataTable.stories.tsx +213 -0
  40. package/src/components/data-table/dataTable.props.ts +69 -0
  41. package/src/components/data-table/dataTable.tsx +313 -0
  42. package/src/components/date-picker/date-picker.props.ts +30 -0
  43. package/src/components/date-picker/date-picker.stories.tsx +90 -0
  44. package/src/components/date-picker/datePicker.tsx +307 -0
  45. package/src/components/dialog/dialog.props.ts +9 -0
  46. package/src/components/dialog/dialog.stories.tsx +80 -0
  47. package/src/components/dialog/dialog.tsx +88 -0
  48. package/src/components/divider/divider.props.ts +8 -0
  49. package/src/components/divider/divider.stories.tsx +34 -0
  50. package/src/components/divider/divider.tsx +21 -0
  51. package/src/components/drawer/drawer.props.ts +14 -0
  52. package/src/components/drawer/drawer.stories.tsx +41 -0
  53. package/src/components/drawer/drawer.tsx +53 -0
  54. package/src/components/dropfile/dropfile.stories.tsx +75 -0
  55. package/src/components/dropfile/dropfile.tsx +407 -0
  56. package/src/components/empty-state/empty-state.props.ts +9 -0
  57. package/src/components/empty-state/empty-state.stories.tsx +20 -0
  58. package/src/components/empty-state/empty-state.tsx +21 -0
  59. package/src/components/flex/flex.props.ts +22 -0
  60. package/src/components/flex/flex.stories.tsx +71 -0
  61. package/src/components/flex/flex.tsx +79 -0
  62. package/src/components/form-builder/fieldRenderer.tsx +218 -0
  63. package/src/components/form-builder/formBuilder.context.tsx +70 -0
  64. package/src/components/form-builder/formBuilder.props.ts +43 -0
  65. package/src/components/form-builder/formBuilder.stories.tsx +317 -0
  66. package/src/components/form-builder/formBuilder.tsx +186 -0
  67. package/src/components/form-builder/useFormBuilder.ts +80 -0
  68. package/src/components/form-header/form-header.props.ts +5 -0
  69. package/src/components/form-header/form-header.tsx +38 -0
  70. package/src/components/grid/grid.props.ts +17 -0
  71. package/src/components/grid/grid.stories.tsx +72 -0
  72. package/src/components/grid/grid.tsx +69 -0
  73. package/src/components/image/image.props.ts +7 -0
  74. package/src/components/image/image.tsx +38 -0
  75. package/src/components/input/input.props.ts +49 -0
  76. package/src/components/input/input.stories.tsx +115 -0
  77. package/src/components/input/input.tsx +615 -0
  78. package/src/components/layout/layout.props.ts +10 -0
  79. package/src/components/layout/layout.stories.tsx +114 -0
  80. package/src/components/layout/layout.tsx +80 -0
  81. package/src/components/loader/loader.props.ts +8 -0
  82. package/src/components/loader/loader.stories.tsx +105 -0
  83. package/src/components/loader/loader.tsx +108 -0
  84. package/src/components/navbar/navbar.props.ts +37 -0
  85. package/src/components/navbar/navbar.tsx +328 -0
  86. package/src/components/page/page.props.ts +19 -0
  87. package/src/components/page/page.stories.tsx +98 -0
  88. package/src/components/page/page.tsx +90 -0
  89. package/src/components/page-header/page-header.props.ts +11 -0
  90. package/src/components/page-header/page-header.stories.tsx +61 -0
  91. package/src/components/page-header/page-header.tsx +62 -0
  92. package/src/components/pagination/pagination.props.ts +53 -0
  93. package/src/components/pagination/pagination.stories.tsx +111 -0
  94. package/src/components/pagination/pagination.tsx +241 -0
  95. package/src/components/popover/popover.props.ts +12 -0
  96. package/src/components/popover/popover.stories.tsx +25 -0
  97. package/src/components/popover/popover.tsx +45 -0
  98. package/src/components/progress/progress.props.ts +12 -0
  99. package/src/components/progress/progress.stories.tsx +40 -0
  100. package/src/components/progress/progress.tsx +52 -0
  101. package/src/components/radio/radio.props.ts +16 -0
  102. package/src/components/radio/radio.stories.tsx +50 -0
  103. package/src/components/radio/radio.tsx +58 -0
  104. package/src/components/search-select/index.ts +2 -0
  105. package/src/components/search-select/search-select.props.ts +46 -0
  106. package/src/components/search-select/search-select.stories.tsx +129 -0
  107. package/src/components/search-select/search-select.tsx +229 -0
  108. package/src/components/searchTable/components/EditableCell.tsx +149 -0
  109. package/src/components/searchTable/components/PaginationControls.tsx +86 -0
  110. package/src/components/searchTable/components/PaginationInfo.tsx +20 -0
  111. package/src/components/searchTable/components/SearchAndSortBar.tsx +53 -0
  112. package/src/components/searchTable/components/SearchInput.tsx +33 -0
  113. package/src/components/searchTable/components/SortButton.tsx +50 -0
  114. package/src/components/searchTable/components/TableEmptyState.tsx +22 -0
  115. package/src/components/searchTable/components/TableHeader.tsx +35 -0
  116. package/src/components/searchTable/components/TableHeaderCell.tsx +43 -0
  117. package/src/components/searchTable/components/TableRow.tsx +144 -0
  118. package/src/components/searchTable/searchTable.props.ts +56 -0
  119. package/src/components/searchTable/searchTable.tsx +187 -0
  120. package/src/components/segmented-control/segmented-control.props.ts +18 -0
  121. package/src/components/segmented-control/segmented-control.stories.tsx +63 -0
  122. package/src/components/segmented-control/segmented-control.tsx +52 -0
  123. package/src/components/select/select.props.ts +25 -0
  124. package/src/components/select/select.stories.tsx +86 -0
  125. package/src/components/select/select.tsx +150 -0
  126. package/src/components/sidebar/sidebar.props.ts +28 -0
  127. package/src/components/sidebar/sidebar.stories.tsx +117 -0
  128. package/src/components/sidebar/sidebar.tsx +313 -0
  129. package/src/components/skeleton/skeleton.props.ts +12 -0
  130. package/src/components/skeleton/skeleton.stories.tsx +30 -0
  131. package/src/components/skeleton/skeleton.tsx +45 -0
  132. package/src/components/slide/slide.props.ts +45 -0
  133. package/src/components/slide/slide.stories.tsx +121 -0
  134. package/src/components/slide/slide.tsx +109 -0
  135. package/src/components/slider/slider.props.ts +10 -0
  136. package/src/components/slider/slider.stories.tsx +30 -0
  137. package/src/components/slider/slider.tsx +49 -0
  138. package/src/components/stack/stack.props.ts +19 -0
  139. package/src/components/stack/stack.stories.tsx +79 -0
  140. package/src/components/stack/stack.tsx +79 -0
  141. package/src/components/stat-card/stat-card.props.ts +13 -0
  142. package/src/components/stat-card/stat-card.stories.tsx +41 -0
  143. package/src/components/stat-card/stat-card.tsx +44 -0
  144. package/src/components/stepper/stepper.css +26 -0
  145. package/src/components/stepper/stepper.props.ts +29 -0
  146. package/src/components/stepper/stepper.stories.tsx +155 -0
  147. package/src/components/stepper/stepper.tsx +227 -0
  148. package/src/components/table/table.props.ts +43 -0
  149. package/src/components/table/table.stories.tsx +189 -0
  150. package/src/components/table/table.tsx +376 -0
  151. package/src/components/tabs/tabs.props.ts +18 -0
  152. package/src/components/tabs/tabs.stories.tsx +32 -0
  153. package/src/components/tabs/tabs.tsx +74 -0
  154. package/src/components/text/text.props.ts +9 -0
  155. package/src/components/text/text.tsx +20 -0
  156. package/src/components/textarea/textarea.props.ts +15 -0
  157. package/src/components/textarea/textarea.stories.tsx +27 -0
  158. package/src/components/textarea/textarea.tsx +55 -0
  159. package/src/components/theme-provider/themeProvider.props.ts +28 -0
  160. package/src/components/theme-provider/themeProvider.tsx +1854 -0
  161. package/src/components/time-picker/timePicker.props.ts +16 -0
  162. package/src/components/time-picker/timePicker.stories.tsx +131 -0
  163. package/src/components/time-picker/timePicker.tsx +317 -0
  164. package/src/components/toast/toast.css +32 -0
  165. package/src/components/toast/toast.props.ts +13 -0
  166. package/src/components/toast/toast.stories.tsx +138 -0
  167. package/src/components/toast/toast.tsx +87 -0
  168. package/src/components/tooltip/tooltip.props.ts +11 -0
  169. package/src/components/tooltip/tooltip.stories.tsx +20 -0
  170. package/src/components/tooltip/tooltip.tsx +55 -0
  171. package/src/components/topbar/topbar.props.ts +21 -0
  172. package/src/components/topbar/topbar.stories.tsx +80 -0
  173. package/src/components/topbar/topbar.tsx +205 -0
  174. package/src/components/triple-filter/tripleFilter.props.ts +15 -0
  175. package/src/components/triple-filter/tripleFilter.stories.tsx +32 -0
  176. package/src/components/triple-filter/tripleFilter.tsx +50 -0
  177. package/src/dev.css +2 -0
  178. package/src/hooks/useClickOutside.ts +21 -0
  179. package/src/hooks/useDebouncedSearch.ts +55 -0
  180. package/src/hooks/useEditableRow.ts +157 -0
  181. package/src/hooks/useTableState.ts +122 -0
  182. package/src/index.css +168 -0
  183. package/src/index.ts +165 -0
  184. package/src/main.tsx +9 -0
  185. package/src/showcases/DataShowcases.tsx +260 -0
  186. package/src/showcases/FeedbackShowcases.tsx +268 -0
  187. package/src/showcases/FormShowcases.tsx +1159 -0
  188. package/src/showcases/HomeShowcase.tsx +324 -0
  189. package/src/showcases/LayoutPrimitivesShowcases.tsx +569 -0
  190. package/src/showcases/NavigationShowcases.tsx +193 -0
  191. package/src/showcases/PageShowcases.tsx +207 -0
  192. package/src/showcases/ShowcaseLayout.tsx +139 -0
  193. package/src/showcases/StructureShowcases.tsx +152 -0
  194. package/src/types/badget.types.ts +37 -0
  195. package/src/types/button.types.ts +16 -0
  196. package/src/types/colors.types.ts +3 -0
  197. package/src/types/field.types.ts +103 -0
  198. package/src/types/formik.types.ts +15 -0
  199. package/src/types/input.types.ts +14 -0
  200. package/src/types/loader.types.ts +9 -0
  201. package/src/types/sizes.types.ts +1 -0
  202. package/src/types/table.types.ts +15 -0
  203. package/src/types/toast.types.ts +8 -0
  204. package/src/types/yup.types.ts +11 -0
  205. package/src/utils/color.utils.ts +99 -0
  206. package/src/utils/styles.ts +120 -0
  207. package/src/utils/table.utils.ts +10 -0
@@ -0,0 +1,608 @@
1
+ import React, { useMemo, useState, useEffect } from 'react';
2
+ import {
3
+ format,
4
+ addDays,
5
+ startOfWeek,
6
+ eachDayOfInterval,
7
+ endOfWeek,
8
+ isSameDay,
9
+ isToday,
10
+ startOfDay,
11
+ parseISO,
12
+ differenceInMinutes,
13
+ addMinutes,
14
+ startOfMonth,
15
+ endOfMonth,
16
+ addMonths,
17
+ isSameMonth,
18
+ isBefore,
19
+ isAfter
20
+ } from 'date-fns';
21
+ import { es } from 'date-fns/locale';
22
+ import { clsx, type ClassValue } from 'clsx';
23
+ import { twMerge } from 'tailwind-merge';
24
+ import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
25
+ import { ITCalendarProps, CalendarEvent } from './calendar.props';
26
+ import ITText from "@/components/text/text";
27
+
28
+ function cn(...inputs: ClassValue[]) {
29
+ return twMerge(clsx(inputs));
30
+ }
31
+
32
+ // ----------------------------------------------------------------------
33
+ // Constants
34
+ // ----------------------------------------------------------------------
35
+
36
+ const START_HOUR = 6; // 6 AM
37
+ const END_HOUR = 22; // 10 PM
38
+ const HOURS_COUNT = END_HOUR - START_HOUR;
39
+
40
+ // Generate time slots
41
+ const TIME_SLOTS = Array.from({ length: HOURS_COUNT + 1 }, (_, i) => START_HOUR + i);
42
+
43
+ // ----------------------------------------------------------------------
44
+ // Component
45
+ // ----------------------------------------------------------------------
46
+
47
+ export const ITCalendar: React.FC<ITCalendarProps> = ({
48
+ events = [],
49
+ mode: modeProp,
50
+ onEventClick,
51
+ onSlotClick,
52
+ onSlotHover,
53
+ onSelectRange,
54
+ value,
55
+ onChange,
56
+ selectionMode = 'single',
57
+ startDate,
58
+ endDate,
59
+ minDate,
60
+ maxDate,
61
+ className,
62
+ variant = 'primary',
63
+ }) => {
64
+ // Determine mode: if onChange provided, assume picker (month) unless specialized
65
+ const mode = modeProp || (onChange ? 'month' : 'week');
66
+
67
+ const [currentDate, setCurrentDate] = useState(value || new Date());
68
+ const [view, setView] = useState<'calendar' | 'years'>('calendar');
69
+
70
+ // Selection/Accent colors based on variant
71
+ const getVariantStyles = () => {
72
+ const v = variant || 'primary';
73
+ return {
74
+ '--calendar-selected-bg': `var(--color-${v})`,
75
+ '--calendar-range-bg': `var(--color-${v}-50)`,
76
+ '--calendar-today-bg': `var(--color-${v}-100)`,
77
+ '--calendar-today-text': `var(--color-${v})`,
78
+ } as React.CSSProperties;
79
+ };
80
+
81
+ // Sync internal state if value changes (for picker)
82
+ useEffect(() => {
83
+ if (value) setCurrentDate(value);
84
+ }, [value]);
85
+
86
+ // Navigation handlers
87
+ const handleNext = () => {
88
+ if (view === 'years') {
89
+ setCurrentDate((d) => {
90
+ const newDate = new Date(d);
91
+ newDate.setFullYear(d.getFullYear() + 12);
92
+ return newDate;
93
+ });
94
+ } else if (mode === 'month') {
95
+ setCurrentDate((d) => addMonths(d, 1));
96
+ } else if (mode === 'day') {
97
+ setCurrentDate((d) => addDays(d, 1));
98
+ } else {
99
+ setCurrentDate((d) => addDays(d, 7));
100
+ }
101
+ };
102
+
103
+ const handlePrev = () => {
104
+ if (view === 'years') {
105
+ setCurrentDate((d) => {
106
+ const newDate = new Date(d);
107
+ newDate.setFullYear(d.getFullYear() - 12);
108
+ return newDate;
109
+ });
110
+ } else if (mode === 'month') {
111
+ setCurrentDate((d) => addMonths(d, -1));
112
+ } else if (mode === 'day') {
113
+ setCurrentDate((d) => addDays(d, -1));
114
+ } else {
115
+ setCurrentDate((d) => addDays(d, -7));
116
+ }
117
+ };
118
+
119
+ const handleToday = () => {
120
+ setCurrentDate(new Date());
121
+ setView('calendar');
122
+ };
123
+
124
+ // ----------------------------------------------------------------------
125
+ // Week / Day View Logic
126
+ // ----------------------------------------------------------------------
127
+
128
+ const viewDays = useMemo(() => {
129
+ if (mode === 'day') {
130
+ return [currentDate];
131
+ }
132
+ const start = startOfWeek(currentDate, { weekStartsOn: 1 });
133
+ const end = endOfWeek(currentDate, { weekStartsOn: 1 });
134
+ return eachDayOfInterval({ start, end });
135
+ }, [currentDate, mode]);
136
+
137
+ const getEventStyle = (event: CalendarEvent) => {
138
+ const start = typeof event.start === 'string' ? parseISO(event.start) : event.start;
139
+ const end = typeof event.end === 'string' ? parseISO(event.end) : event.end;
140
+
141
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
142
+ const dayStartMinutes = START_HOUR * 60;
143
+
144
+ const duration = differenceInMinutes(end, start);
145
+
146
+ return {
147
+ top: `${((startMinutes - dayStartMinutes) / 60) * 80}px`,
148
+ height: `${(duration / 60) * 80}px`,
149
+ };
150
+ };
151
+
152
+ const weekEvents = useMemo(() => {
153
+ return events.filter(event => {
154
+ const eventStart = typeof event.start === 'string' ? parseISO(event.start) : event.start;
155
+ return viewDays.some(day => isSameDay(day, eventStart));
156
+ });
157
+ }, [events, viewDays]);
158
+
159
+
160
+ // ----------------------------------------------------------------------
161
+ // Month View Logic (Picker)
162
+ // ----------------------------------------------------------------------
163
+
164
+ const monthDays = useMemo(() => {
165
+ const start = startOfWeek(startOfMonth(currentDate), { weekStartsOn: 1 });
166
+ const end = endOfWeek(endOfMonth(currentDate), { weekStartsOn: 1 });
167
+ return eachDayOfInterval({ start, end });
168
+ }, [currentDate]);
169
+
170
+ const isDateDisabled = (date: Date) => {
171
+ if (minDate && isBefore(date, startOfDay(minDate))) return true;
172
+ if (maxDate && isAfter(date, startOfDay(maxDate))) return true;
173
+ return false;
174
+ };
175
+
176
+ const [dragStart, setDragStart] = useState<Date | null>(null);
177
+ const [dragCurrent, setDragCurrent] = useState<Date | null>(null);
178
+ const isDraggingRef = React.useRef(false);
179
+
180
+ const handleMouseDown = (date: Date, e: React.MouseEvent) => {
181
+ // Only enable drag if onSelectRange is provided
182
+ if (!onSelectRange) return;
183
+ // We do NOT stopPropagation here completely because we might want other things?
184
+ // Actually for drag we probably want to claim it.
185
+ e.stopPropagation();
186
+ e.preventDefault(); // Prevent text selection
187
+
188
+ isDraggingRef.current = false;
189
+ setDragStart(date);
190
+ setDragCurrent(date);
191
+ };
192
+
193
+ const handleMouseEnter = (date: Date) => {
194
+ if (onSlotHover) {
195
+ onSlotHover(date);
196
+ }
197
+ if (dragStart) {
198
+ isDraggingRef.current = true;
199
+ setDragCurrent(date);
200
+ }
201
+ };
202
+
203
+ // This handles the end of the drag (assigned to container)
204
+ const handleMouseUp = () => {
205
+ if (dragStart && dragCurrent && onSelectRange && isDraggingRef.current) {
206
+ // Normalize start/end
207
+ let start = dragStart;
208
+ let end = dragCurrent;
209
+ if (isBefore(end, start)) {
210
+ [start, end] = [end, start];
211
+ }
212
+ // End date should include the selected slot duration (add 30 mins)
213
+ const finalEnd = addMinutes(end, 30);
214
+
215
+ if (!isSameDay(start, finalEnd) && differenceInMinutes(finalEnd, start) > 0) {
216
+ // Ensure we don't accidentally select across days if logic isn't robust
217
+ // But currently `dragCurrent` comes from same view.
218
+ // Just call it.
219
+ onSelectRange(start, finalEnd);
220
+ } else {
221
+ onSelectRange(start, finalEnd);
222
+ }
223
+ }
224
+
225
+ // We don't reset isDraggingRef.current here immediately if we want to block the subsequent click?
226
+ // But clearing dragStart will be checked by onClick?
227
+ // No, onClick checks !dragStart... but dragStart becomes null here.
228
+ // So onClick needs to check isDraggingRef.
229
+
230
+ setDragStart(null);
231
+ setDragCurrent(null);
232
+ };
233
+
234
+ // ----------------------------------------------------------------------
235
+ // Year View Data
236
+ // ----------------------------------------------------------------------
237
+ const startYear = currentDate.getFullYear() - 6;
238
+ const years = Array.from({ length: 12 }, (_, i) => startYear + i);
239
+
240
+ // ----------------------------------------------------------------------
241
+ // Render
242
+ // ----------------------------------------------------------------------
243
+
244
+ return (
245
+ <div
246
+ className={cn("flex flex-col h-full rounded-lg shadow-sm overflow-hidden select-none", className)}
247
+ style={{
248
+ backgroundColor: 'var(--calendar-bg, #ffffff)',
249
+ border: '1px solid var(--calendar-border, #e2e8f0)',
250
+ ...getVariantStyles(),
251
+ }}
252
+ onMouseUp={handleMouseUp}
253
+ onMouseLeave={() => {
254
+ setDragStart(null);
255
+ setDragCurrent(null);
256
+ }}
257
+ >
258
+
259
+ {/* Header */}
260
+ <div
261
+ className="flex items-center justify-between px-2 py-2 border-b"
262
+ style={{
263
+ backgroundColor: 'var(--calendar-bg, #ffffff)',
264
+ borderBottomColor: 'var(--calendar-border, #e2e8f0)'
265
+ }}
266
+ >
267
+ <ITText
268
+ as="h2"
269
+ className="text-sm font-bold capitalize cursor-pointer transition-colors select-none px-2 py-1 rounded"
270
+ style={{
271
+ color: 'var(--calendar-header-text, #1e293b)',
272
+ }}
273
+ onClick={() => setView(view === 'calendar' ? 'years' : 'calendar')}
274
+ onMouseEnter={(e) => {
275
+ e.currentTarget.style.backgroundColor = 'var(--calendar-header-hover, #f1f5f9)';
276
+ }}
277
+ onMouseLeave={(e) => {
278
+ e.currentTarget.style.backgroundColor = 'transparent';
279
+ }}
280
+ >
281
+ {view === 'years'
282
+ ? `${years[0]} - ${years[years.length - 1]}`
283
+ : format(currentDate, 'MMMM yyyy', { locale: es })
284
+ }
285
+ </ITText>
286
+ <div className="flex items-center gap-1">
287
+ <button
288
+ onClick={handlePrev}
289
+ type="button"
290
+ className="p-1.5 rounded-md transition-colors"
291
+ style={{ color: 'var(--calendar-days-text, #334155)' }}
292
+ onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'var(--calendar-header-hover, #f1f5f9)'; }}
293
+ onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
294
+ >
295
+ <FaChevronLeft size={14} />
296
+ </button>
297
+ <button
298
+ onClick={handleToday}
299
+ type="button"
300
+ className="text-sm font-medium px-3 py-1.5 rounded-md transition-colors"
301
+ style={{ color: 'var(--calendar-days-text, #334155)' }}
302
+ onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'var(--calendar-header-hover, #f1f5f9)'; }}
303
+ onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
304
+ >
305
+ <ITText as="span">Hoy</ITText>
306
+ </button>
307
+ <button
308
+ onClick={handleNext}
309
+ type="button"
310
+ className="p-1.5 rounded-md transition-colors"
311
+ style={{ color: 'var(--calendar-days-text, #334155)' }}
312
+ onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'var(--calendar-header-hover, #f1f5f9)'; }}
313
+ onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
314
+ >
315
+ <FaChevronRight size={14} />
316
+ </button>
317
+ </div>
318
+ </div>
319
+
320
+ {/* Content */}
321
+ <div
322
+ className="flex-1 overflow-auto relative"
323
+ style={{ backgroundColor: 'var(--calendar-bg, #ffffff)' }}
324
+ >
325
+
326
+ {view === 'years' ? (
327
+ <div className="p-4 grid grid-cols-4 gap-2">
328
+ {years.map(year => (
329
+ <button
330
+ key={year}
331
+ type="button"
332
+ className={cn(
333
+ "h-10 rounded-md text-sm font-medium transition-colors border border-transparent",
334
+ year === currentDate.getFullYear()
335
+ ? "bg-[var(--calendar-selected-bg)] text-[var(--calendar-selected-text)]"
336
+ : "hover:bg-[var(--calendar-today-bg)] hover:text-[var(--calendar-today-text)]"
337
+ )}
338
+ style={{
339
+ color: year === currentDate.getFullYear()
340
+ ? 'var(--calendar-selected-text, #ffffff)'
341
+ : 'var(--calendar-days-text, #334155)'
342
+ }}
343
+ onClick={() => {
344
+ setCurrentDate((d) => {
345
+ const newDate = new Date(d);
346
+ newDate.setFullYear(year);
347
+ return newDate;
348
+ });
349
+ setView('calendar');
350
+ }}
351
+ >
352
+ <ITText as="span">{year}</ITText>
353
+ </button>
354
+ ))}
355
+ </div>
356
+ ) : mode === 'month' ? (
357
+ <div className="p-4">
358
+ {/* Weekday Headers */}
359
+ <div className="grid grid-cols-7 mb-2">
360
+ {['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'].map(day => (
361
+ <ITText key={day} as="div" className="text-center text-xs font-semibold text-gray-400 uppercase py-1">
362
+ {day}
363
+ </ITText>
364
+ ))}
365
+ </div>
366
+ {/* Days Grid */}
367
+ <div className="grid grid-cols-7 gap-1">
368
+ {monthDays.map((day) => {
369
+ const isDisabled = isDateDisabled(day);
370
+ const isCurrentMonth = isSameMonth(day, currentDate);
371
+
372
+ // Selection logic
373
+ const isSelected = selectionMode === 'single' && value && isSameDay(day, value);
374
+ const isRangeStart = selectionMode === 'range' && startDate && isSameDay(day, startDate);
375
+ const isRangeEnd = selectionMode === 'range' && endDate && isSameDay(day, endDate);
376
+ const isInRange = selectionMode === 'range' && startDate && endDate && isAfter(day, startDate) && isBefore(day, endDate);
377
+
378
+ return (
379
+ <button
380
+ key={day.toISOString()}
381
+ type="button"
382
+ disabled={isDisabled}
383
+ onClick={() => onChange && onChange(day)}
384
+ className={cn(
385
+ "h-10 w-full flex items-center justify-center rounded-md text-sm transition-colors relative",
386
+ !isCurrentMonth && "opacity-40",
387
+ isDisabled && "opacity-20 cursor-not-allowed",
388
+ )}
389
+ style={{
390
+ backgroundColor: isSelected || isRangeStart || isRangeEnd
391
+ ? 'var(--calendar-selected-bg, #2563eb)'
392
+ : isInRange
393
+ ? 'var(--calendar-range-bg, #eff6ff)'
394
+ : isToday(day)
395
+ ? 'var(--calendar-today-bg, #eff6ff)'
396
+ : 'transparent',
397
+ color: isSelected || isRangeStart || isRangeEnd
398
+ ? 'var(--calendar-selected-text, #ffffff)'
399
+ : isToday(day)
400
+ ? 'var(--calendar-today-text, #2563eb)'
401
+ : 'var(--calendar-days-text, #334155)',
402
+ fontWeight: isSelected || isRangeStart || isRangeEnd || isToday(day) ? '700' : '400',
403
+ }}
404
+ >
405
+ <ITText as="span">{format(day, 'd')}</ITText>
406
+
407
+ {/* Connection for range selection to make it look continuous */}
408
+ {selectionMode === 'range' && isRangeStart && endDate && (
409
+ <div className="absolute right-0 top-0 bottom-0 w-2 bg-[var(--calendar-range-bg)] -z-10" />
410
+ )}
411
+ {selectionMode === 'range' && isRangeEnd && startDate && (
412
+ <div className="absolute left-0 top-0 bottom-0 w-2 bg-[var(--calendar-range-bg)] -z-10" />
413
+ )}
414
+ </button>
415
+ );
416
+ })}
417
+ </div>
418
+ </div>
419
+ ) : (
420
+ /* Week/Day View (Scheduler) */
421
+ <div className={cn("flex h-full", mode === 'week' ? "min-w-[800px]" : "w-full")}>
422
+ {/* Time Sidebar */}
423
+ <div
424
+ className="flex-none w-16 pt-10 select-none"
425
+ style={{
426
+ backgroundColor: 'var(--calendar-header-hover, #f1f5f9)',
427
+ borderRight: '1px solid var(--calendar-border, #e2e8f0)',
428
+ }}
429
+ >
430
+ {TIME_SLOTS.map((hour) => (
431
+ hour < END_HOUR && (
432
+ <div key={hour} className="h-20 relative text-right pr-2">
433
+ <ITText as="span" className="text-xs text-slate-400 dark:text-slate-500 -mt-2 inline-block transform -translate-y-1/2">
434
+ {format(new Date().setHours(hour, 0), 'HH:mm')}
435
+ </ITText>
436
+ </div>
437
+ )
438
+ ))}
439
+ </div>
440
+
441
+ {/* Days Columns */}
442
+ <div className="flex flex-1">
443
+ {viewDays.map((day) => (
444
+ <div
445
+ key={day.toISOString()}
446
+ className="flex-1 min-w-[120px] relative"
447
+ style={{ borderRight: '1px solid var(--calendar-border, #e2e8f0)' }}
448
+ >
449
+
450
+ {/* Day Header */}
451
+ <div
452
+ className="h-10 flex flex-col items-center justify-center sticky top-0 z-10"
453
+ style={{
454
+ backgroundColor: isToday(day) ? 'var(--calendar-today-bg, #eff6ff)' : 'var(--calendar-bg, #ffffff)',
455
+ borderBottom: '1px solid var(--calendar-border, #e2e8f0)',
456
+ }}
457
+ >
458
+ <ITText
459
+ as="span"
460
+ className="text-xs font-semibold uppercase"
461
+ style={{
462
+ color: isToday(day) ? 'var(--calendar-today-text, #2563eb)' : 'var(--calendar-days-text, #334155)',
463
+ opacity: isToday(day) ? 1 : 0.6,
464
+ }}
465
+ >
466
+ {format(day, 'EEE', { locale: es })}
467
+ </ITText>
468
+ <ITText
469
+ as="span"
470
+ className="text-sm font-bold w-6 h-6 flex items-center justify-center rounded-full mt-0.5"
471
+ style={{
472
+ color: isToday(day) ? 'var(--calendar-selected-text, #ffffff)' : 'var(--calendar-days-text, #334155)',
473
+ backgroundColor: isToday(day) ? 'var(--calendar-selected-bg, #2563eb)' : 'transparent',
474
+ }}
475
+ >
476
+ {format(day, 'd')}
477
+ </ITText>
478
+ </div>
479
+
480
+ {/* Slots Grid */}
481
+ <div className="relative">
482
+ {TIME_SLOTS.map((hour) => (
483
+ hour < END_HOUR && (
484
+ <div
485
+ key={hour}
486
+ className="h-20 relative group"
487
+ style={{ borderBottom: '1px dashed var(--calendar-border, #e2e8f0)' }}
488
+ >
489
+ {/* Slot 00 */}
490
+ <div
491
+ className="absolute inset-x-0 top-0 h-10 border-b border-transparent hover:border-[var(--calendar-today-bg)] hover:bg-[var(--calendar-today-bg)] transition-colors cursor-pointer z-0"
492
+ onMouseDown={(e) => {
493
+ const d = new Date(day); d.setHours(hour, 0, 0, 0);
494
+ handleMouseDown(d, e);
495
+ }}
496
+ onMouseEnter={() => {
497
+ const d = new Date(day); d.setHours(hour, 0, 0, 0);
498
+ handleMouseEnter(d);
499
+ }}
500
+ onClick={() => {
501
+ if (!isDraggingRef.current) {
502
+ const d = new Date(day); d.setHours(hour, 0, 0, 0);
503
+ onSlotClick && onSlotClick(d);
504
+ }
505
+ }}
506
+ />
507
+ {/* Slot 30 */}
508
+ <div
509
+ className="absolute inset-x-0 bottom-0 h-10 hover:border-[var(--calendar-today-bg)] hover:bg-[var(--calendar-today-bg)] transition-colors cursor-pointer z-0"
510
+ onMouseDown={(e) => {
511
+ const d = new Date(day); d.setHours(hour, 30, 0, 0);
512
+ handleMouseDown(d, e);
513
+ }}
514
+ onMouseEnter={() => {
515
+ const d = new Date(day); d.setHours(hour, 30, 0, 0);
516
+ handleMouseEnter(d);
517
+ }}
518
+ onClick={() => {
519
+ if (!isDraggingRef.current) {
520
+ const d = new Date(day); d.setHours(hour, 30, 0, 0);
521
+ onSlotClick && onSlotClick(d);
522
+ }
523
+ }}
524
+ />
525
+ </div>
526
+ )
527
+ ))}
528
+
529
+ {/* Selection Overlay */}
530
+ {dragStart && dragCurrent && isSameDay(dragStart, day) && (
531
+ (() => {
532
+ let start = dragStart;
533
+ let end = dragCurrent;
534
+ if (isBefore(end, start)) [start, end] = [end, start];
535
+ const finalEnd = addMinutes(end, 30); // Visual end is end of slot
536
+
537
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
538
+ const dayStartMinutes = START_HOUR * 60;
539
+ const duration = differenceInMinutes(finalEnd, start);
540
+ const top = ((startMinutes - dayStartMinutes) / 60) * 80;
541
+ const height = (duration / 60) * 80;
542
+
543
+ return (
544
+ <div
545
+ className="absolute left-1 right-1 bg-[var(--calendar-selected-bg)]/30 border border-[var(--calendar-selected-bg)] rounded z-10 pointer-events-none"
546
+ style={{ top: `${top}px`, height: `${height}px` }}
547
+ />
548
+ );
549
+ })()
550
+ )}
551
+
552
+ {/* Events */}
553
+ {weekEvents
554
+ .filter((event) => isSameDay(typeof event.start === 'string' ? parseISO(event.start) : event.start, day))
555
+ .map((event) => {
556
+ const style = getEventStyle(event);
557
+ return (
558
+ <div
559
+ key={event.id}
560
+ className={cn(
561
+ "absolute left-1 right-1 rounded px-2 py-1 text-xs cursor-pointer hover:brightness-95 transition-all shadow-sm overflow-hidden z-20 border-l-4",
562
+ !event.color && "bg-[var(--calendar-today-bg)] text-[var(--calendar-today-text)] border-[var(--calendar-selected-bg)]"
563
+ )}
564
+ style={{
565
+ top: style.top,
566
+ height: style.height,
567
+ backgroundColor: event.color ? `${event.color}20` : undefined,
568
+ borderColor: event.color,
569
+ color: event.color ? event.color : undefined
570
+ }}
571
+ onClick={(e) => {
572
+ e.stopPropagation();
573
+ onEventClick && onEventClick(event);
574
+ }}
575
+ >
576
+ <ITText as="div" className="font-semibold truncate">{event.title}</ITText>
577
+ <ITText as="div" className="opacity-80 truncate">
578
+ {format(typeof event.start === 'string' ? parseISO(event.start) : event.start, 'HH:mm')} -
579
+ {format(typeof event.end === 'string' ? parseISO(event.end) : event.end, 'HH:mm')}
580
+ </ITText>
581
+ </div>
582
+ );
583
+ })}
584
+ </div>
585
+
586
+ {/* Current Time Line */}
587
+ {isToday(day) && (
588
+ <div
589
+ className="absolute left-0 right-0 border-t-2 border-danger-500 z-30 pointer-events-none"
590
+ style={{
591
+ top: `${((new Date().getHours() * 60 + new Date().getMinutes() - (START_HOUR * 60)) / 60) * 80}px`
592
+ }}
593
+ >
594
+ <div className="absolute -left-1.5 -top-1.5 w-3 h-3 bg-danger-500 rounded-full" />
595
+ </div>
596
+ )}
597
+
598
+ </div>
599
+ ))}
600
+ </div>
601
+ </div>
602
+ )}
603
+ </div>
604
+ </div>
605
+ );
606
+ };
607
+
608
+ export default ITCalendar;
@@ -0,0 +1,3 @@
1
+ export { default } from './calendar';
2
+ export * from './calendar';
3
+
@@ -0,0 +1,13 @@
1
+ export interface ITCardProps {
2
+ onClick?: () => void;
3
+ title?: string;
4
+ image?: string;
5
+ alt?: string;
6
+ children?: React.ReactNode;
7
+ actions?: React.ReactNode;
8
+ className?: string;
9
+ imageClassName?: string;
10
+ titleClassName?: string;
11
+ contentClassName?: string;
12
+ actionClassName?: string;
13
+ }
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import ITCard from './card';
3
+ import ITButton from '../button/button';
4
+
5
+ const meta = {
6
+ title: 'Components/Layout & Navigation/ITCard',
7
+ component: ITCard,
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ onClick: { action: 'clicked' },
14
+ },
15
+ } satisfies Meta<typeof ITCard>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ args: {
22
+ title: 'Card Title',
23
+ children: 'This is the body of the card. It has padding defined by the theme.',
24
+ onClick: undefined,
25
+ },
26
+ };
27
+
28
+ export const WithImage: Story = {
29
+ args: {
30
+ title: 'Card with Image',
31
+ image: 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
32
+ children: 'A nice gradient image above.',
33
+ onClick: undefined,
34
+ },
35
+ };
36
+
37
+ export const WithActions: Story = {
38
+ args: {
39
+ title: 'Card with Actions',
40
+ children: 'This card has a button action at the bottom.',
41
+ actions: (
42
+ <div className="flex justify-end gap-2">
43
+ <ITButton variant="text" color="secondary" label="Cancel" />
44
+ <ITButton variant="filled" color="primary" label="Save" />
45
+ </div>
46
+ ),
47
+ onClick: undefined,
48
+ },
49
+ };
50
+
51
+ export const Clickable: Story = {
52
+ args: {
53
+ title: 'Clickable Card',
54
+ children: 'Hover over me! I should have a stronger shadow and a pointer cursor.',
55
+ onClick: () => alert('Card clicked!'),
56
+ image: 'https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
57
+ },
58
+ };