@pattern-stack/frontend-patterns 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/index.es.js +1 -1
  2. package/dist/index.js +1 -0
  3. package/package.json +5 -3
  4. package/src/App.css +42 -0
  5. package/src/App.tsx +54 -0
  6. package/src/__tests__/README.md +221 -0
  7. package/src/__tests__/atoms/hooks/simple-hooks.test.ts +44 -0
  8. package/src/__tests__/atoms/ui/button.test.tsx +68 -0
  9. package/src/__tests__/atoms/utils/simple.test.ts +18 -0
  10. package/src/__tests__/atoms/utils/utils.test.ts +77 -0
  11. package/src/__tests__/features/auth/simple-auth.test.tsx +40 -0
  12. package/src/__tests__/molecules/layout/simple-layout.test.tsx +81 -0
  13. package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +167 -0
  14. package/src/__tests__/setup.ts +51 -0
  15. package/src/__tests__/utils.tsx +123 -0
  16. package/src/atoms/composed/Accordion/Accordion.tsx +271 -0
  17. package/src/atoms/composed/Accordion/index.ts +1 -0
  18. package/src/atoms/composed/Alert/Alert.tsx +132 -0
  19. package/src/atoms/composed/Alert/index.ts +1 -0
  20. package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +83 -0
  21. package/src/atoms/composed/Breadcrumb/index.ts +1 -0
  22. package/src/atoms/composed/Chart/Chart.tsx +425 -0
  23. package/src/atoms/composed/Chart/index.ts +2 -0
  24. package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +72 -0
  25. package/src/atoms/composed/ColorSwatch/index.ts +1 -0
  26. package/src/atoms/composed/DarkModeToggle.tsx +66 -0
  27. package/src/atoms/composed/DataBadge/DataBadge.tsx +81 -0
  28. package/src/atoms/composed/DataBadge/index.ts +1 -0
  29. package/src/atoms/composed/DataTable/DataTable.tsx +394 -0
  30. package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +41 -0
  31. package/src/atoms/composed/DataTable/index.ts +2 -0
  32. package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +611 -0
  33. package/src/atoms/composed/DateTimePicker/index.ts +2 -0
  34. package/src/atoms/composed/DetailedCard/DetailedCard.tsx +181 -0
  35. package/src/atoms/composed/DetailedCard/index.ts +2 -0
  36. package/src/atoms/composed/EmptyState/EmptyState.tsx +90 -0
  37. package/src/atoms/composed/EmptyState/index.ts +1 -0
  38. package/src/atoms/composed/FileUpload/FileUpload.tsx +477 -0
  39. package/src/atoms/composed/FileUpload/index.ts +2 -0
  40. package/src/atoms/composed/FormField/FormField.tsx +92 -0
  41. package/src/atoms/composed/FormField/index.ts +1 -0
  42. package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +37 -0
  43. package/src/atoms/composed/GlobalSearch/index.ts +1 -0
  44. package/src/atoms/composed/IconBadge/IconBadge.tsx +95 -0
  45. package/src/atoms/composed/IconBadge/index.ts +2 -0
  46. package/src/atoms/composed/Modal/Modal.tsx +223 -0
  47. package/src/atoms/composed/Modal/index.ts +2 -0
  48. package/src/atoms/composed/PaletteSwitcher.tsx +386 -0
  49. package/src/atoms/composed/ProgressBar/ProgressBar.tsx +116 -0
  50. package/src/atoms/composed/ProgressBar/index.ts +1 -0
  51. package/src/atoms/composed/StatCard/StatCard.tsx +219 -0
  52. package/src/atoms/composed/StatCard/index.ts +1 -0
  53. package/src/atoms/composed/StyleGuide.tsx +717 -0
  54. package/src/atoms/composed/Toast/Toast.tsx +219 -0
  55. package/src/atoms/composed/Toast/index.ts +1 -0
  56. package/src/atoms/composed/Tooltip/Tooltip.tsx +213 -0
  57. package/src/atoms/composed/Tooltip/index.ts +1 -0
  58. package/src/atoms/composed/UserAvatar/UserAvatar.tsx +139 -0
  59. package/src/atoms/composed/UserAvatar/index.ts +1 -0
  60. package/src/atoms/composed/UserMenu/UserMenu.tsx +16 -0
  61. package/src/atoms/composed/UserMenu/index.ts +1 -0
  62. package/src/atoms/composed/index.ts +29 -0
  63. package/src/atoms/hooks/useApi.ts +80 -0
  64. package/src/atoms/hooks/useHealth.ts +17 -0
  65. package/src/atoms/index.ts +13 -0
  66. package/src/atoms/services/api/client.ts +134 -0
  67. package/src/atoms/services/auth-service.ts +248 -0
  68. package/src/atoms/services/health.ts +15 -0
  69. package/src/atoms/services/index.ts +3 -0
  70. package/src/atoms/shared/config/constants.ts +17 -0
  71. package/src/atoms/shared/config/dashboard-sizes.ts +111 -0
  72. package/src/atoms/shared/config/environment.ts +10 -0
  73. package/src/atoms/shared/index.ts +4 -0
  74. package/src/atoms/shared/styles/color-palettes.css +566 -0
  75. package/src/atoms/types/auth.ts +62 -0
  76. package/src/atoms/types/generated.ts +1469 -0
  77. package/src/atoms/types/index.ts +4 -0
  78. package/src/atoms/types/loading.ts +28 -0
  79. package/src/atoms/ui/Badge.tsx +30 -0
  80. package/src/atoms/ui/ErrorBoundary.tsx +59 -0
  81. package/src/atoms/ui/Select.tsx +53 -0
  82. package/src/atoms/ui/Switch.tsx +42 -0
  83. package/src/atoms/ui/Tabs.tsx +118 -0
  84. package/src/atoms/ui/avatar.tsx +48 -0
  85. package/src/atoms/ui/button.tsx +70 -0
  86. package/src/atoms/ui/card.tsx +76 -0
  87. package/src/atoms/ui/dropdown-menu.tsx +199 -0
  88. package/src/atoms/ui/index.ts +39 -0
  89. package/src/atoms/ui/input.tsx +23 -0
  90. package/src/atoms/ui/label.tsx +23 -0
  91. package/src/atoms/ui/skeleton.tsx +13 -0
  92. package/src/atoms/ui/spinner.tsx +49 -0
  93. package/src/atoms/ui/table.tsx +116 -0
  94. package/src/atoms/utils/animations.ts +135 -0
  95. package/src/atoms/utils/tooltip-helpers.ts +140 -0
  96. package/src/atoms/utils/utils.ts +9 -0
  97. package/src/features/auth/components/LoginForm.tsx +168 -0
  98. package/src/features/auth/components/LogoutButton.tsx +19 -0
  99. package/src/features/auth/components/ProtectedRoute.tsx +60 -0
  100. package/src/features/auth/components/index.ts +4 -0
  101. package/src/features/auth/hooks/index.ts +2 -0
  102. package/src/features/auth/hooks/useAuth.tsx +205 -0
  103. package/src/features/auth/hooks/usePermissions.ts +35 -0
  104. package/src/features/auth/index.ts +2 -0
  105. package/src/features/index.ts +2 -0
  106. package/src/index.css +704 -0
  107. package/src/index.ts +13 -0
  108. package/src/main.tsx +48 -0
  109. package/src/molecules/.gitkeep +0 -0
  110. package/src/molecules/forms/FormGroup.tsx +75 -0
  111. package/src/molecules/forms/SearchInput.tsx +259 -0
  112. package/src/molecules/forms/index.ts +4 -0
  113. package/src/molecules/index.ts +4 -0
  114. package/src/molecules/layout/AppHeader/AppHeader.tsx +42 -0
  115. package/src/molecules/layout/AppHeader/index.ts +1 -0
  116. package/src/molecules/layout/AppLayout.tsx +29 -0
  117. package/src/molecules/layout/PageTemplate.tsx +87 -0
  118. package/src/molecules/layout/SectionHeader/SectionHeader.tsx +87 -0
  119. package/src/molecules/layout/SectionHeader/index.ts +1 -0
  120. package/src/molecules/layout/ShowcaseSection.tsx +57 -0
  121. package/src/molecules/layout/Sidebar.tsx +144 -0
  122. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +99 -0
  123. package/src/molecules/layout/SidebarButton/index.ts +1 -0
  124. package/src/molecules/layout/SidebarContext.tsx +31 -0
  125. package/src/molecules/layout/index.ts +7 -0
  126. package/src/molecules/navigation/NavMenu.tsx +188 -0
  127. package/src/molecules/navigation/Pagination.tsx +172 -0
  128. package/src/molecules/navigation/index.ts +4 -0
  129. package/src/organisms/index.ts +5 -0
  130. package/src/organisms/showcase/ComponentShowcasePage.tsx +2496 -0
  131. package/src/organisms/showcase/index.ts +1 -0
  132. package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +242 -0
  133. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +171 -0
  134. package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +385 -0
  135. package/src/pages/AdminShowcase/index.tsx +3 -0
  136. package/src/pages/ComponentShowcase/BadgesShowcase.tsx +188 -0
  137. package/src/pages/ComponentShowcase/CardsShowcase.tsx +392 -0
  138. package/src/pages/ComponentShowcase/PalettesShowcase.tsx +207 -0
  139. package/src/pages/ComponentShowcase/StatesShowcase.tsx +485 -0
  140. package/src/pages/ComponentShowcase/TablesShowcase.tsx +134 -0
  141. package/src/pages/ComponentShowcase/TypographyShowcase.tsx +255 -0
  142. package/src/pages/ComponentShowcase/index.tsx +188 -0
  143. package/src/pages/index.ts +2 -0
  144. package/src/templates/AuthTemplate.tsx +216 -0
  145. package/src/templates/ComponentShowcaseTemplate.tsx +173 -0
  146. package/src/templates/DashboardTemplate.tsx +232 -0
  147. package/src/templates/DataTemplate.tsx +319 -0
  148. package/src/templates/admin/AdminCRUDTemplate.tsx +630 -0
  149. package/src/templates/admin/AdminDashboardTemplate.tsx +351 -0
  150. package/src/templates/admin/AdminDetailTemplate.tsx +563 -0
  151. package/src/templates/admin/index.ts +29 -0
  152. package/src/templates/factory.tsx +169 -0
  153. package/src/templates/index.ts +37 -0
  154. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,611 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { Calendar, Clock, ChevronLeft, ChevronRight, X } from 'lucide-react';
3
+ import { cn } from '../../utils/utils';
4
+ import { Input } from '../../ui/input';
5
+ import { Button } from '../../ui/button';
6
+
7
+ export interface DateTimePickerProps {
8
+ /** Current selected date/time value */
9
+ value?: Date;
10
+ /** Callback when date/time changes */
11
+ onChange?: (date: Date | null) => void;
12
+ /** Picker mode */
13
+ mode?: 'date' | 'time' | 'datetime';
14
+ /** Visual variant */
15
+ variant?: 'default' | 'compact' | 'inline';
16
+ /** Date format for display */
17
+ dateFormat?: 'MM/dd/yyyy' | 'dd/MM/yyyy' | 'yyyy-MM-dd';
18
+ /** Time format */
19
+ timeFormat?: '12h' | '24h';
20
+ /** Minimum selectable date */
21
+ minDate?: Date;
22
+ /** Maximum selectable date */
23
+ maxDate?: Date;
24
+ /** Enable date range selection */
25
+ isRange?: boolean;
26
+ /** Range values for range mode */
27
+ rangeValue?: { start: Date | null; end: Date | null };
28
+ /** Callback for range changes */
29
+ onRangeChange?: (range: { start: Date | null; end: Date | null }) => void;
30
+ /** Placeholder text */
31
+ placeholder?: string;
32
+ /** Disabled state */
33
+ disabled?: boolean;
34
+ /** Error state */
35
+ error?: boolean;
36
+ /** Clear button */
37
+ clearable?: boolean;
38
+ /** Additional CSS classes */
39
+ className?: string;
40
+ /** ARIA label */
41
+ 'aria-label'?: string;
42
+ }
43
+
44
+ const MONTHS = [
45
+ 'January', 'February', 'March', 'April', 'May', 'June',
46
+ 'July', 'August', 'September', 'October', 'November', 'December'
47
+ ];
48
+
49
+ const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
50
+
51
+ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
52
+ value,
53
+ onChange,
54
+ mode = 'datetime',
55
+ variant = 'default',
56
+ dateFormat = 'MM/dd/yyyy',
57
+ timeFormat = '12h',
58
+ minDate,
59
+ maxDate,
60
+ isRange = false,
61
+ rangeValue,
62
+ onRangeChange,
63
+ placeholder,
64
+ disabled = false,
65
+ error = false,
66
+ clearable = true,
67
+ className,
68
+ 'aria-label': ariaLabel,
69
+ ...props
70
+ }) => {
71
+ const [isOpen, setIsOpen] = useState(false);
72
+ const [viewDate, setViewDate] = useState(value || new Date());
73
+ const [, setActiveInput] = useState<'start' | 'end' | null>(null);
74
+ const [tempTime, setTempTime] = useState({
75
+ hours: value?.getHours() || 0,
76
+ minutes: value?.getMinutes() || 0
77
+ });
78
+
79
+ const containerRef = useRef<HTMLDivElement>(null);
80
+ const inputRef = useRef<HTMLInputElement>(null);
81
+
82
+ // Close picker when clicking outside
83
+ useEffect(() => {
84
+ const handleClickOutside = (event: MouseEvent) => {
85
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
86
+ setIsOpen(false);
87
+ setActiveInput(null);
88
+ }
89
+ };
90
+
91
+ document.addEventListener('mousedown', handleClickOutside);
92
+ return () => document.removeEventListener('mousedown', handleClickOutside);
93
+ }, []);
94
+
95
+ // Format date for display
96
+ const formatDate = (date: Date | null): string => {
97
+ if (!date) return '';
98
+
99
+ const day = date.getDate().toString().padStart(2, '0');
100
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
101
+ const year = date.getFullYear();
102
+
103
+ switch (dateFormat) {
104
+ case 'dd/MM/yyyy':
105
+ return `${day}/${month}/${year}`;
106
+ case 'yyyy-MM-dd':
107
+ return `${year}-${month}-${day}`;
108
+ default:
109
+ return `${month}/${day}/${year}`;
110
+ }
111
+ };
112
+
113
+ // Format time for display
114
+ const formatTime = (date: Date | null): string => {
115
+ if (!date) return '';
116
+
117
+ const hours = date.getHours();
118
+ const minutes = date.getMinutes().toString().padStart(2, '0');
119
+
120
+ if (timeFormat === '12h') {
121
+ const displayHours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
122
+ const ampm = hours >= 12 ? 'PM' : 'AM';
123
+ return `${displayHours}:${minutes} ${ampm}`;
124
+ }
125
+
126
+ return `${hours.toString().padStart(2, '0')}:${minutes}`;
127
+ };
128
+
129
+ // Format complete datetime
130
+ const formatDateTime = (date: Date | null): string => {
131
+ if (!date) return '';
132
+
133
+ const datePart = mode === 'time' ? '' : formatDate(date);
134
+ const timePart = mode === 'date' ? '' : formatTime(date);
135
+
136
+ if (mode === 'datetime') {
137
+ return `${datePart} ${timePart}`;
138
+ }
139
+
140
+ return datePart || timePart;
141
+ };
142
+
143
+ // Get display value for input
144
+ const getDisplayValue = (): string => {
145
+ if (isRange && rangeValue) {
146
+ const start = rangeValue.start ? formatDateTime(rangeValue.start) : '';
147
+ const end = rangeValue.end ? formatDateTime(rangeValue.end) : '';
148
+ return start && end ? `${start} - ${end}` : start || end || '';
149
+ }
150
+
151
+ return formatDateTime(value || null);
152
+ };
153
+
154
+ // Generate calendar days
155
+ const getCalendarDays = () => {
156
+ const year = viewDate.getFullYear();
157
+ const month = viewDate.getMonth();
158
+
159
+ const firstDay = new Date(year, month, 1);
160
+ const startDate = new Date(firstDay);
161
+ startDate.setDate(startDate.getDate() - firstDay.getDay());
162
+
163
+ const days: Date[] = [];
164
+
165
+ for (let i = 0; i < 42; i++) {
166
+ const currentDate = new Date(startDate);
167
+ currentDate.setDate(startDate.getDate() + i);
168
+ days.push(currentDate);
169
+ }
170
+
171
+ return days;
172
+ };
173
+
174
+ // Handle date selection
175
+ const handleDateSelect = (date: Date) => {
176
+ if (disabled) return;
177
+
178
+ // Check date constraints
179
+ if (minDate && date < minDate) return;
180
+ if (maxDate && date > maxDate) return;
181
+
182
+ if (isRange && onRangeChange) {
183
+ if (!rangeValue?.start || (rangeValue.start && rangeValue.end)) {
184
+ // Start new range
185
+ onRangeChange({ start: date, end: null });
186
+ setActiveInput('end');
187
+ } else if (rangeValue.start && !rangeValue.end) {
188
+ // Complete range
189
+ if (date >= rangeValue.start) {
190
+ onRangeChange({ start: rangeValue.start, end: date });
191
+ } else {
192
+ onRangeChange({ start: date, end: rangeValue.start });
193
+ }
194
+ setActiveInput(null);
195
+ if (mode === 'date') setIsOpen(false);
196
+ }
197
+ } else {
198
+ // Single date selection
199
+ const newDate = new Date(date);
200
+ if (value && (mode === 'datetime' || mode === 'time')) {
201
+ // Preserve time when selecting date
202
+ newDate.setHours(value.getHours());
203
+ newDate.setMinutes(value.getMinutes());
204
+ }
205
+
206
+ onChange?.(newDate);
207
+ if (mode === 'date') setIsOpen(false);
208
+ }
209
+ };
210
+
211
+ // Handle time change
212
+ const handleTimeChange = (hours: number, minutes: number) => {
213
+ if (disabled) return;
214
+
215
+ const newDate = value ? new Date(value) : new Date();
216
+ newDate.setHours(hours);
217
+ newDate.setMinutes(minutes);
218
+
219
+ setTempTime({ hours, minutes });
220
+ onChange?.(newDate);
221
+ };
222
+
223
+ // Handle input click
224
+ const handleInputClick = () => {
225
+ if (!disabled) {
226
+ setIsOpen(true);
227
+ if (isRange) setActiveInput('start');
228
+ }
229
+ };
230
+
231
+ // Handle clear
232
+ const handleClear = () => {
233
+ if (isRange && onRangeChange) {
234
+ onRangeChange({ start: null, end: null });
235
+ } else {
236
+ onChange?.(null);
237
+ }
238
+ setIsOpen(false);
239
+ };
240
+
241
+ // Navigate calendar
242
+ const navigateMonth = (direction: 'prev' | 'next') => {
243
+ const newDate = new Date(viewDate);
244
+ newDate.setMonth(viewDate.getMonth() + (direction === 'prev' ? -1 : 1));
245
+ setViewDate(newDate);
246
+ };
247
+
248
+ // Check if date is selected
249
+ const isDateSelected = (date: Date): boolean => {
250
+ if (isRange && rangeValue) {
251
+ return (rangeValue.start ? isSameDay(date, rangeValue.start) : false) ||
252
+ (rangeValue.end ? isSameDay(date, rangeValue.end) : false);
253
+ }
254
+ return value ? isSameDay(date, value) : false;
255
+ };
256
+
257
+ // Check if date is in range
258
+ const isDateInRange = (date: Date): boolean => {
259
+ if (isRange && rangeValue?.start && rangeValue?.end) {
260
+ return date >= rangeValue.start && date <= rangeValue.end;
261
+ }
262
+ return false;
263
+ };
264
+
265
+ // Utility function to check if dates are same day
266
+ const isSameDay = (date1: Date, date2: Date): boolean => {
267
+ return date1.getDate() === date2.getDate() &&
268
+ date1.getMonth() === date2.getMonth() &&
269
+ date1.getFullYear() === date2.getFullYear();
270
+ };
271
+
272
+ // Check if date is today
273
+ const isToday = (date: Date): boolean => {
274
+ return isSameDay(date, new Date());
275
+ };
276
+
277
+ // Check if date is disabled
278
+ const isDateDisabled = (date: Date): boolean => {
279
+ if (minDate && date < minDate) return true;
280
+ if (maxDate && date > maxDate) return true;
281
+ return false;
282
+ };
283
+
284
+ const variantClasses = {
285
+ default: 'h-10',
286
+ compact: 'h-8 text-sm',
287
+ inline: 'border-0 bg-transparent p-0'
288
+ };
289
+
290
+ const calendarDays = getCalendarDays();
291
+
292
+ if (variant === 'inline') {
293
+ return (
294
+ <div
295
+ className={cn('inline-block', className)}
296
+ data-component-name="DateTimePicker"
297
+ ref={containerRef}
298
+ >
299
+ {/* Inline Calendar */}
300
+ <div className="bg-card border border-border rounded p-4 shadow-category-1">
301
+ {(mode === 'date' || mode === 'datetime') && (
302
+ <div className="space-y-4">
303
+ {/* Calendar Header */}
304
+ <div className="flex items-center justify-between">
305
+ <Button
306
+ variant="ghost"
307
+ size="sm"
308
+ onClick={() => navigateMonth('prev')}
309
+ disabled={disabled}
310
+ className="h-8 w-8 p-0"
311
+ >
312
+ <ChevronLeft className="w-4 h-4" />
313
+ </Button>
314
+ <h3 className="text-sm font-semibold text-foreground">
315
+ {MONTHS[viewDate.getMonth()]} {viewDate.getFullYear()}
316
+ </h3>
317
+ <Button
318
+ variant="ghost"
319
+ size="sm"
320
+ onClick={() => navigateMonth('next')}
321
+ disabled={disabled}
322
+ className="h-8 w-8 p-0"
323
+ >
324
+ <ChevronRight className="w-4 h-4" />
325
+ </Button>
326
+ </div>
327
+
328
+ {/* Calendar Grid */}
329
+ <div className="grid grid-cols-7 gap-1">
330
+ {/* Weekday headers */}
331
+ {WEEKDAYS.map(day => (
332
+ <div key={day} className="text-xs font-medium text-muted-foreground text-center p-2">
333
+ {day}
334
+ </div>
335
+ ))}
336
+
337
+ {/* Calendar days */}
338
+ {calendarDays.map((date, index) => {
339
+ const isCurrentMonth = date.getMonth() === viewDate.getMonth();
340
+ const isSelected = isDateSelected(date);
341
+ const isInRange = isDateInRange(date);
342
+ const isTodayDate = isToday(date);
343
+ const isDisabled = isDateDisabled(date);
344
+
345
+ return (
346
+ <button
347
+ key={index}
348
+ onClick={() => handleDateSelect(date)}
349
+ disabled={disabled || isDisabled}
350
+ className={cn(
351
+ 'h-8 w-8 text-xs rounded transition-colors',
352
+ 'hover:bg-accent hover:text-accent-foreground',
353
+ 'focus:outline-none focus:ring-2 focus:ring-primary/20',
354
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
355
+ !isCurrentMonth && 'text-muted-foreground/50',
356
+ isSelected && 'bg-primary text-primary-foreground hover:bg-primary-hover',
357
+ isInRange && !isSelected && 'bg-primary/20',
358
+ isTodayDate && !isSelected && 'bg-accent text-accent-foreground font-semibold'
359
+ )}
360
+ >
361
+ {date.getDate()}
362
+ </button>
363
+ );
364
+ })}
365
+ </div>
366
+ </div>
367
+ )}
368
+
369
+ {/* Time Picker for inline datetime/time mode */}
370
+ {(mode === 'time' || mode === 'datetime') && (
371
+ <div className={cn('space-y-3', mode === 'datetime' && 'border-t border-border pt-4 mt-4')}>
372
+ <div className="flex items-center gap-2">
373
+ <Clock className="w-4 h-4 text-muted-foreground" />
374
+ <span className="text-sm font-medium text-foreground">Time</span>
375
+ </div>
376
+
377
+ <div className="flex items-center gap-2">
378
+ <select
379
+ value={tempTime.hours}
380
+ onChange={(e) => handleTimeChange(parseInt(e.target.value), tempTime.minutes)}
381
+ disabled={disabled}
382
+ className="px-2 py-1 text-sm border border-border rounded bg-background text-foreground"
383
+ >
384
+ {Array.from({ length: 24 }, (_, i) => (
385
+ <option key={i} value={i}>
386
+ {timeFormat === '12h'
387
+ ? (i === 0 ? '12' : i > 12 ? i - 12 : i)
388
+ : i.toString().padStart(2, '0')
389
+ }
390
+ </option>
391
+ ))}
392
+ </select>
393
+ <span className="text-foreground">:</span>
394
+ <select
395
+ value={tempTime.minutes}
396
+ onChange={(e) => handleTimeChange(tempTime.hours, parseInt(e.target.value))}
397
+ disabled={disabled}
398
+ className="px-2 py-1 text-sm border border-border rounded bg-background text-foreground"
399
+ >
400
+ {Array.from({ length: 60 }, (_, i) => (
401
+ <option key={i} value={i}>
402
+ {i.toString().padStart(2, '0')}
403
+ </option>
404
+ ))}
405
+ </select>
406
+ {timeFormat === '12h' && (
407
+ <select
408
+ value={tempTime.hours >= 12 ? 'PM' : 'AM'}
409
+ onChange={(e) => {
410
+ const isPM = e.target.value === 'PM';
411
+ const newHours = isPM
412
+ ? (tempTime.hours % 12) + 12
413
+ : tempTime.hours % 12;
414
+ handleTimeChange(newHours, tempTime.minutes);
415
+ }}
416
+ disabled={disabled}
417
+ className="px-2 py-1 text-sm border border-border rounded bg-background text-foreground"
418
+ >
419
+ <option value="AM">AM</option>
420
+ <option value="PM">PM</option>
421
+ </select>
422
+ )}
423
+ </div>
424
+ </div>
425
+ )}
426
+ </div>
427
+ </div>
428
+ );
429
+ }
430
+
431
+ return (
432
+ <div
433
+ className={cn('relative', className)}
434
+ data-component-name="DateTimePicker"
435
+ ref={containerRef}
436
+ >
437
+ {/* Input Field */}
438
+ <div className="relative">
439
+ <Input
440
+ ref={inputRef}
441
+ value={getDisplayValue()}
442
+ onClick={handleInputClick}
443
+ placeholder={placeholder || `Select ${mode}...`}
444
+ readOnly
445
+ disabled={disabled}
446
+ aria-label={ariaLabel}
447
+ className={cn(
448
+ variantClasses[variant],
449
+ 'pr-20 cursor-pointer',
450
+ error && 'border-status-error focus:border-status-error focus:ring-status-error/20',
451
+ disabled && 'cursor-not-allowed'
452
+ )}
453
+ {...props}
454
+ />
455
+
456
+ {/* Input Icons */}
457
+ <div className="absolute inset-y-0 right-0 flex items-center pr-3 gap-1">
458
+ {clearable && getDisplayValue() && !disabled && (
459
+ <Button
460
+ variant="ghost"
461
+ size="sm"
462
+ onClick={handleClear}
463
+ className="h-6 w-6 p-0 hover:bg-muted"
464
+ >
465
+ <X className="w-4 h-4" />
466
+ </Button>
467
+ )}
468
+
469
+ <div className="text-muted-foreground">
470
+ {mode === 'time' ? (
471
+ <Clock className="w-4 h-4" />
472
+ ) : (
473
+ <Calendar className="w-4 h-4" />
474
+ )}
475
+ </div>
476
+ </div>
477
+ </div>
478
+
479
+ {/* Dropdown Picker */}
480
+ {isOpen && (
481
+ <div className="absolute top-full left-0 z-50 mt-1 bg-card border border-border rounded shadow-category-1 p-4 min-w-[300px]">
482
+ {(mode === 'date' || mode === 'datetime') && (
483
+ <div className="space-y-4">
484
+ {/* Calendar Header */}
485
+ <div className="flex items-center justify-between">
486
+ <Button
487
+ variant="ghost"
488
+ size="sm"
489
+ onClick={() => navigateMonth('prev')}
490
+ disabled={disabled}
491
+ className="h-8 w-8 p-0"
492
+ >
493
+ <ChevronLeft className="w-4 h-4" />
494
+ </Button>
495
+ <h3 className="text-sm font-semibold text-foreground">
496
+ {MONTHS[viewDate.getMonth()]} {viewDate.getFullYear()}
497
+ </h3>
498
+ <Button
499
+ variant="ghost"
500
+ size="sm"
501
+ onClick={() => navigateMonth('next')}
502
+ disabled={disabled}
503
+ className="h-8 w-8 p-0"
504
+ >
505
+ <ChevronRight className="w-4 h-4" />
506
+ </Button>
507
+ </div>
508
+
509
+ {/* Calendar Grid */}
510
+ <div className="grid grid-cols-7 gap-1">
511
+ {/* Weekday headers */}
512
+ {WEEKDAYS.map(day => (
513
+ <div key={day} className="text-xs font-medium text-muted-foreground text-center p-2">
514
+ {day}
515
+ </div>
516
+ ))}
517
+
518
+ {/* Calendar days */}
519
+ {calendarDays.map((date, index) => {
520
+ const isCurrentMonth = date.getMonth() === viewDate.getMonth();
521
+ const isSelected = isDateSelected(date);
522
+ const isInRange = isDateInRange(date);
523
+ const isTodayDate = isToday(date);
524
+ const isDisabled = isDateDisabled(date);
525
+
526
+ return (
527
+ <button
528
+ key={index}
529
+ onClick={() => handleDateSelect(date)}
530
+ disabled={disabled || isDisabled}
531
+ className={cn(
532
+ 'h-8 w-8 text-xs rounded transition-colors',
533
+ 'hover:bg-accent hover:text-accent-foreground',
534
+ 'focus:outline-none focus:ring-2 focus:ring-primary/20',
535
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
536
+ !isCurrentMonth && 'text-muted-foreground/50',
537
+ isSelected && 'bg-primary text-primary-foreground hover:bg-primary-hover',
538
+ isInRange && !isSelected && 'bg-primary/20',
539
+ isTodayDate && !isSelected && 'bg-accent text-accent-foreground font-semibold'
540
+ )}
541
+ >
542
+ {date.getDate()}
543
+ </button>
544
+ );
545
+ })}
546
+ </div>
547
+ </div>
548
+ )}
549
+
550
+ {/* Time Picker */}
551
+ {(mode === 'time' || mode === 'datetime') && (
552
+ <div className={cn('space-y-3', mode === 'datetime' && 'border-t border-border pt-4 mt-4')}>
553
+ <div className="flex items-center gap-2">
554
+ <Clock className="w-4 h-4 text-muted-foreground" />
555
+ <span className="text-sm font-medium text-foreground">Time</span>
556
+ </div>
557
+
558
+ <div className="flex items-center gap-2">
559
+ <select
560
+ value={tempTime.hours}
561
+ onChange={(e) => handleTimeChange(parseInt(e.target.value), tempTime.minutes)}
562
+ disabled={disabled}
563
+ className="px-2 py-1 text-sm border border-border rounded bg-background text-foreground"
564
+ >
565
+ {Array.from({ length: 24 }, (_, i) => (
566
+ <option key={i} value={i}>
567
+ {timeFormat === '12h'
568
+ ? (i === 0 ? '12' : i > 12 ? i - 12 : i)
569
+ : i.toString().padStart(2, '0')
570
+ }
571
+ </option>
572
+ ))}
573
+ </select>
574
+ <span className="text-foreground">:</span>
575
+ <select
576
+ value={tempTime.minutes}
577
+ onChange={(e) => handleTimeChange(tempTime.hours, parseInt(e.target.value))}
578
+ disabled={disabled}
579
+ className="px-2 py-1 text-sm border border-border rounded bg-background text-foreground"
580
+ >
581
+ {Array.from({ length: 60 }, (_, i) => (
582
+ <option key={i} value={i}>
583
+ {i.toString().padStart(2, '0')}
584
+ </option>
585
+ ))}
586
+ </select>
587
+ {timeFormat === '12h' && (
588
+ <select
589
+ value={tempTime.hours >= 12 ? 'PM' : 'AM'}
590
+ onChange={(e) => {
591
+ const isPM = e.target.value === 'PM';
592
+ const newHours = isPM
593
+ ? (tempTime.hours % 12) + 12
594
+ : tempTime.hours % 12;
595
+ handleTimeChange(newHours, tempTime.minutes);
596
+ }}
597
+ disabled={disabled}
598
+ className="px-2 py-1 text-sm border border-border rounded bg-background text-foreground"
599
+ >
600
+ <option value="AM">AM</option>
601
+ <option value="PM">PM</option>
602
+ </select>
603
+ )}
604
+ </div>
605
+ </div>
606
+ )}
607
+ </div>
608
+ )}
609
+ </div>
610
+ );
611
+ };
@@ -0,0 +1,2 @@
1
+ export { DateTimePicker } from './DateTimePicker';
2
+ export type { DateTimePickerProps } from './DateTimePicker';