@thangnqdev/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +196 -0
  2. package/dist/Label-DWOlgSNW.d.cts +14 -0
  3. package/dist/Label-DWOlgSNW.d.ts +14 -0
  4. package/dist/bento/index.cjs +140 -0
  5. package/dist/bento/index.cjs.map +1 -0
  6. package/dist/bento/index.d.cts +4 -0
  7. package/dist/bento/index.d.ts +4 -0
  8. package/dist/bento/index.js +4 -0
  9. package/dist/bento/index.js.map +1 -0
  10. package/dist/bento/styles.css +46 -0
  11. package/dist/buttons/index.cjs +365 -0
  12. package/dist/buttons/index.cjs.map +1 -0
  13. package/dist/buttons/index.d.cts +4 -0
  14. package/dist/buttons/index.d.ts +4 -0
  15. package/dist/buttons/index.js +5 -0
  16. package/dist/buttons/index.js.map +1 -0
  17. package/dist/buttons/styles.css +6 -0
  18. package/dist/chunk-2OXZK6IP.js +514 -0
  19. package/dist/chunk-2OXZK6IP.js.map +1 -0
  20. package/dist/chunk-4S6L5V7X.js +15 -0
  21. package/dist/chunk-4S6L5V7X.js.map +1 -0
  22. package/dist/chunk-5FZ6ZFII.js +728 -0
  23. package/dist/chunk-5FZ6ZFII.js.map +1 -0
  24. package/dist/chunk-66VWUJEI.js +86 -0
  25. package/dist/chunk-66VWUJEI.js.map +1 -0
  26. package/dist/chunk-A7P3O5Z5.js +163 -0
  27. package/dist/chunk-A7P3O5Z5.js.map +1 -0
  28. package/dist/chunk-ATYGDTMK.js +257 -0
  29. package/dist/chunk-ATYGDTMK.js.map +1 -0
  30. package/dist/chunk-C4SUXMI3.js +138 -0
  31. package/dist/chunk-C4SUXMI3.js.map +1 -0
  32. package/dist/chunk-EU2LPUYP.js +25 -0
  33. package/dist/chunk-EU2LPUYP.js.map +1 -0
  34. package/dist/chunk-F5Q7BTMH.js +1991 -0
  35. package/dist/chunk-F5Q7BTMH.js.map +1 -0
  36. package/dist/chunk-GXI2H5XA.js +46 -0
  37. package/dist/chunk-GXI2H5XA.js.map +1 -0
  38. package/dist/chunk-H3LCNPMC.js +101 -0
  39. package/dist/chunk-H3LCNPMC.js.map +1 -0
  40. package/dist/chunk-IABPUCAQ.js +19 -0
  41. package/dist/chunk-IABPUCAQ.js.map +1 -0
  42. package/dist/chunk-KJRBRTQE.js +101 -0
  43. package/dist/chunk-KJRBRTQE.js.map +1 -0
  44. package/dist/chunk-LBUBXUYQ.js +40 -0
  45. package/dist/chunk-LBUBXUYQ.js.map +1 -0
  46. package/dist/chunk-LWFRUJRU.js +537 -0
  47. package/dist/chunk-LWFRUJRU.js.map +1 -0
  48. package/dist/chunk-NFVONJUK.js +882 -0
  49. package/dist/chunk-NFVONJUK.js.map +1 -0
  50. package/dist/chunk-PF4ZHGVW.js +284 -0
  51. package/dist/chunk-PF4ZHGVW.js.map +1 -0
  52. package/dist/chunk-WAFHC4DQ.js +366 -0
  53. package/dist/chunk-WAFHC4DQ.js.map +1 -0
  54. package/dist/chunk-X76KBH4Y.js +71 -0
  55. package/dist/chunk-X76KBH4Y.js.map +1 -0
  56. package/dist/core/index.cjs +103 -0
  57. package/dist/core/index.cjs.map +1 -0
  58. package/dist/core/index.d.cts +3 -0
  59. package/dist/core/index.d.ts +3 -0
  60. package/dist/core/index.js +4 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/core/styles.css +128 -0
  63. package/dist/data-display/index.cjs +1023 -0
  64. package/dist/data-display/index.cjs.map +1 -0
  65. package/dist/data-display/index.d.cts +4 -0
  66. package/dist/data-display/index.d.ts +4 -0
  67. package/dist/data-display/index.js +6 -0
  68. package/dist/data-display/index.js.map +1 -0
  69. package/dist/data-display/styles.css +4 -0
  70. package/dist/feedback/index.cjs +368 -0
  71. package/dist/feedback/index.cjs.map +1 -0
  72. package/dist/feedback/index.d.cts +4 -0
  73. package/dist/feedback/index.d.ts +4 -0
  74. package/dist/feedback/index.js +4 -0
  75. package/dist/feedback/index.js.map +1 -0
  76. package/dist/feedback/styles.css +4 -0
  77. package/dist/forms/index.cjs +2287 -0
  78. package/dist/forms/index.cjs.map +1 -0
  79. package/dist/forms/index.d.cts +5 -0
  80. package/dist/forms/index.d.ts +5 -0
  81. package/dist/forms/index.js +10 -0
  82. package/dist/forms/index.js.map +1 -0
  83. package/dist/forms/styles.css +6 -0
  84. package/dist/glass/index.cjs +165 -0
  85. package/dist/glass/index.cjs.map +1 -0
  86. package/dist/glass/index.d.cts +4 -0
  87. package/dist/glass/index.d.ts +4 -0
  88. package/dist/glass/index.js +4 -0
  89. package/dist/glass/index.js.map +1 -0
  90. package/dist/glass/styles.css +81 -0
  91. package/dist/index-BTvkbCIj.d.cts +146 -0
  92. package/dist/index-BTvkbCIj.d.ts +146 -0
  93. package/dist/index-BkRqGNG1.d.cts +201 -0
  94. package/dist/index-BkRqGNG1.d.ts +201 -0
  95. package/dist/index-BlPcGeW-.d.cts +45 -0
  96. package/dist/index-BlPcGeW-.d.ts +45 -0
  97. package/dist/index-BzOCqvC6.d.cts +32 -0
  98. package/dist/index-BzOCqvC6.d.ts +32 -0
  99. package/dist/index-CCeWYAvn.d.cts +83 -0
  100. package/dist/index-COQ7uwt9.d.cts +44 -0
  101. package/dist/index-COQ7uwt9.d.ts +44 -0
  102. package/dist/index-CenuF_P2.d.cts +221 -0
  103. package/dist/index-CenuF_P2.d.ts +221 -0
  104. package/dist/index-Cle6QzQl.d.ts +371 -0
  105. package/dist/index-D6fE7w-b.d.cts +90 -0
  106. package/dist/index-D6fE7w-b.d.ts +90 -0
  107. package/dist/index-DR8cfJUG.d.ts +83 -0
  108. package/dist/index-DzcWh1Hs.d.cts +131 -0
  109. package/dist/index-DzcWh1Hs.d.ts +131 -0
  110. package/dist/index-TyK-Ldpy.d.cts +371 -0
  111. package/dist/index-ugj5s6a3.d.cts +95 -0
  112. package/dist/index-ugj5s6a3.d.ts +95 -0
  113. package/dist/index.cjs +6400 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +16 -0
  116. package/dist/index.d.ts +16 -0
  117. package/dist/index.js +21 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/layout/index.cjs +516 -0
  120. package/dist/layout/index.cjs.map +1 -0
  121. package/dist/layout/index.d.cts +4 -0
  122. package/dist/layout/index.d.ts +4 -0
  123. package/dist/layout/index.js +4 -0
  124. package/dist/layout/index.js.map +1 -0
  125. package/dist/layout/styles.css +8 -0
  126. package/dist/navigation/index.cjs +673 -0
  127. package/dist/navigation/index.cjs.map +1 -0
  128. package/dist/navigation/index.d.cts +2 -0
  129. package/dist/navigation/index.d.ts +2 -0
  130. package/dist/navigation/index.js +8 -0
  131. package/dist/navigation/index.js.map +1 -0
  132. package/dist/navigation/styles.css +4 -0
  133. package/dist/overlays/index.cjs +954 -0
  134. package/dist/overlays/index.cjs.map +1 -0
  135. package/dist/overlays/index.d.cts +4 -0
  136. package/dist/overlays/index.d.ts +4 -0
  137. package/dist/overlays/index.js +9 -0
  138. package/dist/overlays/index.js.map +1 -0
  139. package/dist/overlays/styles.css +8 -0
  140. package/dist/styles.css +333 -0
  141. package/dist/typography/index.cjs +311 -0
  142. package/dist/typography/index.cjs.map +1 -0
  143. package/dist/typography/index.d.cts +5 -0
  144. package/dist/typography/index.d.ts +5 -0
  145. package/dist/typography/index.js +6 -0
  146. package/dist/typography/index.js.map +1 -0
  147. package/dist/typography/styles.css +5 -0
  148. package/llms.txt +113 -0
  149. package/package.json +116 -0
  150. package/src/bento/BentoCard.tsx +140 -0
  151. package/src/bento/BentoGrid.tsx +50 -0
  152. package/src/bento/index.ts +2 -0
  153. package/src/bento/tokens.css +46 -0
  154. package/src/buttons/Button.tsx +104 -0
  155. package/src/buttons/ButtonGroup.tsx +43 -0
  156. package/src/buttons/IconButton.tsx +79 -0
  157. package/src/buttons/SplitButton.tsx +86 -0
  158. package/src/buttons/ToggleButton.tsx +157 -0
  159. package/src/buttons/index.ts +5 -0
  160. package/src/buttons/tokens.css +6 -0
  161. package/src/core/ThemeProvider.tsx +123 -0
  162. package/src/core/index.ts +2 -0
  163. package/src/core/tokens.css +128 -0
  164. package/src/data-display/Accordion.tsx +147 -0
  165. package/src/data-display/Avatar.tsx +134 -0
  166. package/src/data-display/Card.tsx +112 -0
  167. package/src/data-display/Carousel.tsx +130 -0
  168. package/src/data-display/Chip.tsx +74 -0
  169. package/src/data-display/DataTablePro.tsx +356 -0
  170. package/src/data-display/StatCard.tsx +60 -0
  171. package/src/data-display/Table.tsx +74 -0
  172. package/src/data-display/Timeline.tsx +62 -0
  173. package/src/data-display/TreeView.tsx +140 -0
  174. package/src/data-display/index.ts +10 -0
  175. package/src/data-display/tokens.css +4 -0
  176. package/src/feedback/Alert.tsx +80 -0
  177. package/src/feedback/EmptyState.tsx +45 -0
  178. package/src/feedback/ProgressBar.tsx +174 -0
  179. package/src/feedback/Skeleton.tsx +66 -0
  180. package/src/feedback/Spinner.tsx +65 -0
  181. package/src/feedback/index.ts +5 -0
  182. package/src/feedback/tokens.css +4 -0
  183. package/src/forms/Checkbox.tsx +69 -0
  184. package/src/forms/CheckboxGroup.tsx +29 -0
  185. package/src/forms/Combobox.tsx +368 -0
  186. package/src/forms/DatePicker.tsx +480 -0
  187. package/src/forms/FileUpload.tsx +222 -0
  188. package/src/forms/FormField.tsx +115 -0
  189. package/src/forms/Input.tsx +151 -0
  190. package/src/forms/MaskedInput.tsx +97 -0
  191. package/src/forms/NumberInput.tsx +248 -0
  192. package/src/forms/PasswordInput.tsx +35 -0
  193. package/src/forms/PinInput.tsx +125 -0
  194. package/src/forms/Radio.tsx +142 -0
  195. package/src/forms/RangeSlider.tsx +111 -0
  196. package/src/forms/SearchInput.tsx +34 -0
  197. package/src/forms/Select.tsx +103 -0
  198. package/src/forms/Slider.tsx +85 -0
  199. package/src/forms/Switch.tsx +95 -0
  200. package/src/forms/Textarea.tsx +114 -0
  201. package/src/forms/index.ts +18 -0
  202. package/src/forms/tokens.css +6 -0
  203. package/src/glass/GlassBadge.tsx +61 -0
  204. package/src/glass/GlassButton.tsx +59 -0
  205. package/src/glass/GlassCard.tsx +71 -0
  206. package/src/glass/index.ts +3 -0
  207. package/src/glass/tokens.css +81 -0
  208. package/src/index.ts +26 -0
  209. package/src/layout/AspectRatio.tsx +39 -0
  210. package/src/layout/Container.tsx +53 -0
  211. package/src/layout/Divider.tsx +61 -0
  212. package/src/layout/Flex.tsx +79 -0
  213. package/src/layout/Grid.tsx +124 -0
  214. package/src/layout/PageLayout.tsx +59 -0
  215. package/src/layout/ScrollArea.tsx +43 -0
  216. package/src/layout/Section.tsx +47 -0
  217. package/src/layout/Spacer.tsx +30 -0
  218. package/src/layout/Stack.tsx +88 -0
  219. package/src/layout/index.ts +10 -0
  220. package/src/layout/tokens.css +8 -0
  221. package/src/navigation/Breadcrumb.tsx +73 -0
  222. package/src/navigation/CommandPalette.tsx +142 -0
  223. package/src/navigation/DropdownMenu.tsx +160 -0
  224. package/src/navigation/Pagination.tsx +114 -0
  225. package/src/navigation/Stepper.tsx +93 -0
  226. package/src/navigation/Tabs.tsx +133 -0
  227. package/src/navigation/index.ts +6 -0
  228. package/src/navigation/tokens.css +4 -0
  229. package/src/overlays/AlertDialog.tsx +54 -0
  230. package/src/overlays/BottomSheet.tsx +123 -0
  231. package/src/overlays/ContextMenu.tsx +119 -0
  232. package/src/overlays/Drawer.tsx +145 -0
  233. package/src/overlays/FocusTrap.tsx +49 -0
  234. package/src/overlays/HoverCard.tsx +72 -0
  235. package/src/overlays/Modal.tsx +219 -0
  236. package/src/overlays/Popover.tsx +126 -0
  237. package/src/overlays/Portal.tsx +23 -0
  238. package/src/overlays/Toast.tsx +137 -0
  239. package/src/overlays/Tooltip.tsx +100 -0
  240. package/src/overlays/index.ts +11 -0
  241. package/src/overlays/tokens.css +8 -0
  242. package/src/styles/tokens.css +2 -0
  243. package/src/typography/Blockquote.tsx +32 -0
  244. package/src/typography/Code.tsx +89 -0
  245. package/src/typography/Heading.tsx +56 -0
  246. package/src/typography/Kbd.tsx +25 -0
  247. package/src/typography/Label.tsx +47 -0
  248. package/src/typography/Link.tsx +56 -0
  249. package/src/typography/Text.tsx +62 -0
  250. package/src/typography/index.ts +7 -0
  251. package/src/typography/tokens.css +5 -0
  252. package/src/utils/cn.ts +6 -0
  253. package/ui-contract.json +62 -0
@@ -0,0 +1,480 @@
1
+ import React, { forwardRef, useState, useRef, useEffect } from 'react';
2
+ import { Calendar as CalendarIcon, Clock, ChevronLeft, ChevronRight, X } from 'lucide-react';
3
+ import { cn } from '../utils/cn';
4
+ import { Button } from '../buttons/Button';
5
+ import { Popover } from '../overlays/Popover';
6
+
7
+ // Utility helper to format date as YYYY-MM-DD
8
+ export function formatDate(d: Date | null | undefined): string {
9
+ if (!d) return '';
10
+ const year = d.getFullYear();
11
+ const month = String(d.getMonth() + 1).padStart(2, '0');
12
+ const day = String(d.getDate()).padStart(2, '0');
13
+ return `${year}-${month}-${day}`;
14
+ }
15
+
16
+ export function formatDisplayDate(d: Date | null | undefined): string {
17
+ if (!d) return '';
18
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
19
+ }
20
+
21
+ export interface DatePickerProps {
22
+ value?: Date | null;
23
+ defaultValue?: Date | null;
24
+ onChange?: (date: Date | null) => void;
25
+ placeholder?: string;
26
+ minDate?: Date;
27
+ maxDate?: Date;
28
+ disabled?: boolean;
29
+ clearable?: boolean;
30
+ className?: string;
31
+ presets?: { label: string; date: Date }[];
32
+ }
33
+
34
+ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
35
+ (
36
+ {
37
+ value: controlledValue,
38
+ defaultValue,
39
+ onChange,
40
+ placeholder = 'Select date...',
41
+ minDate,
42
+ maxDate,
43
+ disabled = false,
44
+ clearable = true,
45
+ className,
46
+ presets,
47
+ },
48
+ ref
49
+ ) => {
50
+ const [internalDate, setInternalDate] = useState<Date | null>(
51
+ defaultValue !== undefined ? defaultValue : null
52
+ );
53
+ const selectedDate = controlledValue !== undefined ? controlledValue : internalDate;
54
+
55
+ const [viewDate, setViewDate] = useState<Date>(selectedDate || new Date());
56
+ const [isOpen, setIsOpen] = useState(false);
57
+
58
+ useEffect(() => {
59
+ if (selectedDate) {
60
+ setViewDate(selectedDate);
61
+ }
62
+ }, [selectedDate]);
63
+
64
+ const handleSelectDate = (d: Date) => {
65
+ setInternalDate(d);
66
+ onChange?.(d);
67
+ setIsOpen(false);
68
+ };
69
+
70
+ const handleClear = (e: React.MouseEvent) => {
71
+ e.stopPropagation();
72
+ setInternalDate(null);
73
+ onChange?.(null);
74
+ };
75
+
76
+ const prevMonth = () => {
77
+ setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1));
78
+ };
79
+
80
+ const nextMonth = () => {
81
+ setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1));
82
+ };
83
+
84
+ // Calendar generation logic
85
+ const year = viewDate.getFullYear();
86
+ const month = viewDate.getMonth();
87
+ const firstDayIndex = new Date(year, month, 1).getDay();
88
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
89
+ const daysInPrevMonth = new Date(year, month, 0).getDate();
90
+
91
+ const days: { date: Date; currentMonth: boolean; disabled: boolean }[] = [];
92
+
93
+ // Prev month padding days
94
+ for (let i = firstDayIndex - 1; i >= 0; i--) {
95
+ const d = new Date(year, month - 1, daysInPrevMonth - i);
96
+ const isDis = (minDate && d < minDate) || (maxDate && d > maxDate);
97
+ days.push({ date: d, currentMonth: false, disabled: Boolean(isDis) });
98
+ }
99
+
100
+ // Current month days
101
+ for (let i = 1; i <= daysInMonth; i++) {
102
+ const d = new Date(year, month, i);
103
+ const isDis = (minDate && d < minDate) || (maxDate && d > maxDate);
104
+ days.push({ date: d, currentMonth: true, disabled: Boolean(isDis) });
105
+ }
106
+
107
+ // Next month padding days to complete grid of 35 or 42
108
+ const totalSlots = days.length > 35 ? 42 : 35;
109
+ const remainingSlots = totalSlots - days.length;
110
+ for (let i = 1; i <= remainingSlots; i++) {
111
+ const d = new Date(year, month + 1, i);
112
+ const isDis = (minDate && d < minDate) || (maxDate && d > maxDate);
113
+ days.push({ date: d, currentMonth: false, disabled: Boolean(isDis) });
114
+ }
115
+
116
+ const todayStr = formatDate(new Date());
117
+ const selectedStr = selectedDate ? formatDate(selectedDate) : '';
118
+
119
+ return (
120
+ <div ref={ref} className={cn('relative inline-block w-full', className)}>
121
+ <Popover
122
+ isOpen={isOpen}
123
+ onClose={() => setIsOpen(false)}
124
+ trigger={
125
+ <button
126
+ type="button"
127
+ disabled={disabled}
128
+ onClick={() => !disabled && setIsOpen(!isOpen)}
129
+ className={cn(
130
+ 'w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm transition-all shadow-inner cursor-pointer',
131
+ disabled
132
+ ? 'opacity-50 cursor-not-allowed border-[var(--ui-border-subtle)]'
133
+ : 'border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] focus:ring-2 focus:ring-[var(--ui-primary)]/50'
134
+ )}
135
+ >
136
+ <div className="flex items-center gap-2.5 truncate">
137
+ <CalendarIcon className="w-4 h-4 text-[var(--ui-text-muted)] shrink-0" />
138
+ <span className={selectedDate ? 'text-[var(--ui-text-primary)] font-medium' : 'text-[var(--ui-text-muted)]'}>
139
+ {selectedDate ? formatDisplayDate(selectedDate) : placeholder}
140
+ </span>
141
+ </div>
142
+
143
+ <div className="flex items-center gap-1 text-[var(--ui-text-muted)]">
144
+ {clearable && selectedDate && !disabled && (
145
+ <span
146
+ onClick={handleClear}
147
+ className="p-1 rounded-full hover:bg-[var(--ui-bg-tertiary)] hover:text-[var(--ui-text-primary)] transition-colors"
148
+ >
149
+ <X className="w-3.5 h-3.5" />
150
+ </span>
151
+ )}
152
+ </div>
153
+ </button>
154
+ }
155
+ >
156
+ <div className="p-4 w-72 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-3">
157
+ {/* Header: Month Year + Prev/Next */}
158
+ <div className="flex items-center justify-between">
159
+ <span className="text-xs font-bold text-[var(--ui-text-primary)] font-mono uppercase tracking-wider">
160
+ {viewDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
161
+ </span>
162
+ <div className="flex items-center gap-1">
163
+ <button
164
+ type="button"
165
+ onClick={prevMonth}
166
+ className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] transition-colors"
167
+ >
168
+ <ChevronLeft className="w-4 h-4" />
169
+ </button>
170
+ <button
171
+ type="button"
172
+ onClick={nextMonth}
173
+ className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] transition-colors"
174
+ >
175
+ <ChevronRight className="w-4 h-4" />
176
+ </button>
177
+ </div>
178
+ </div>
179
+
180
+ {/* Presets if provided */}
181
+ {presets && presets.length > 0 && (
182
+ <div className="flex flex-wrap gap-1 pb-2 border-b border-[var(--ui-border-subtle)]">
183
+ {presets.map((preset, idx) => (
184
+ <button
185
+ key={idx}
186
+ type="button"
187
+ onClick={() => handleSelectDate(preset.date)}
188
+ className="px-2 py-0.5 rounded-md text-[10px] font-semibold bg-[var(--ui-bg-secondary)] hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-secondary)] transition-colors"
189
+ >
190
+ {preset.label}
191
+ </button>
192
+ ))}
193
+ </div>
194
+ )}
195
+
196
+ {/* Weekday headers */}
197
+ <div className="grid grid-cols-7 text-center text-[10px] font-bold text-[var(--ui-text-muted)] font-mono">
198
+ {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((d) => (
199
+ <div key={d} className="py-1">
200
+ {d}
201
+ </div>
202
+ ))}
203
+ </div>
204
+
205
+ {/* Day grid */}
206
+ <div className="grid grid-cols-7 gap-1">
207
+ {days.map((item, idx) => {
208
+ const itemStr = formatDate(item.date);
209
+ const isSelected = itemStr === selectedStr;
210
+ const isToday = itemStr === todayStr;
211
+
212
+ return (
213
+ <button
214
+ key={idx}
215
+ type="button"
216
+ disabled={item.disabled}
217
+ onClick={() => handleSelectDate(item.date)}
218
+ className={cn(
219
+ 'h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center transition-all cursor-pointer',
220
+ item.disabled && 'opacity-25 cursor-not-allowed pointer-events-none',
221
+ !item.currentMonth && 'text-[var(--ui-text-muted)] opacity-50',
222
+ isSelected
223
+ ? 'bg-[var(--ui-primary)] text-white font-bold shadow-md shadow-blue-500/30'
224
+ : isToday
225
+ ? 'border border-[var(--ui-primary)] text-[var(--ui-primary)]'
226
+ : 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
227
+ )}
228
+ >
229
+ {item.date.getDate()}
230
+ </button>
231
+ );
232
+ })}
233
+ </div>
234
+ </div>
235
+ </Popover>
236
+ </div>
237
+ );
238
+ }
239
+ );
240
+ DatePicker.displayName = 'DatePicker';
241
+
242
+ /* -------------------------------------------------------------------------------------------------
243
+ * DateRangePicker Component
244
+ * -----------------------------------------------------------------------------------------------*/
245
+ export interface DateRange {
246
+ from: Date | null;
247
+ to: Date | null;
248
+ }
249
+
250
+ export interface DateRangePickerProps {
251
+ value?: DateRange;
252
+ defaultValue?: DateRange;
253
+ onChange?: (range: DateRange) => void;
254
+ placeholder?: string;
255
+ className?: string;
256
+ disabled?: boolean;
257
+ }
258
+
259
+ export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
260
+ ({ value: controlledVal, defaultValue, onChange, placeholder = 'Select date range...', className, disabled = false }, ref) => {
261
+ const [internalRange, setInternalRange] = useState<DateRange>(
262
+ defaultValue || { from: null, to: null }
263
+ );
264
+ const range = controlledVal || internalRange;
265
+ const [viewDate, setViewDate] = useState<Date>(range.from || new Date());
266
+ const [isOpen, setIsOpen] = useState(false);
267
+
268
+ const handleDayClick = (d: Date) => {
269
+ let next: DateRange;
270
+ if (!range.from || (range.from && range.to)) {
271
+ next = { from: d, to: null };
272
+ } else {
273
+ if (d < range.from) {
274
+ next = { from: d, to: range.from };
275
+ } else {
276
+ next = { from: range.from, to: d };
277
+ setIsOpen(false);
278
+ }
279
+ }
280
+ setInternalRange(next);
281
+ onChange?.(next);
282
+ };
283
+
284
+ const displayText =
285
+ range.from && range.to
286
+ ? `${formatDisplayDate(range.from)} – ${formatDisplayDate(range.to)}`
287
+ : range.from
288
+ ? `${formatDisplayDate(range.from)} – ...`
289
+ : placeholder;
290
+
291
+ // Calendar generation
292
+ const year = viewDate.getFullYear();
293
+ const month = viewDate.getMonth();
294
+ const firstDayIndex = new Date(year, month, 1).getDay();
295
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
296
+
297
+ const days: Date[] = [];
298
+ for (let i = 1; i <= daysInMonth; i++) {
299
+ days.push(new Date(year, month, i));
300
+ }
301
+
302
+ return (
303
+ <div ref={ref} className={cn('relative inline-block w-full', className)}>
304
+ <Popover
305
+ isOpen={isOpen}
306
+ onClose={() => setIsOpen(false)}
307
+ trigger={
308
+ <button
309
+ type="button"
310
+ disabled={disabled}
311
+ onClick={() => !disabled && setIsOpen(!isOpen)}
312
+ className="w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm shadow-inner cursor-pointer"
313
+ >
314
+ <div className="flex items-center gap-2.5 truncate">
315
+ <CalendarIcon className="w-4 h-4 text-[var(--ui-text-muted)] shrink-0" />
316
+ <span className={range.from ? 'text-[var(--ui-text-primary)] font-medium' : 'text-[var(--ui-text-muted)]'}>
317
+ {displayText}
318
+ </span>
319
+ </div>
320
+ </button>
321
+ }
322
+ >
323
+ <div className="p-4 w-76 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-3">
324
+ <div className="flex items-center justify-between">
325
+ <span className="text-xs font-bold text-[var(--ui-text-primary)] font-mono uppercase tracking-wider">
326
+ {viewDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
327
+ </span>
328
+ <div className="flex items-center gap-1">
329
+ <button
330
+ type="button"
331
+ onClick={() => setViewDate(new Date(year, month - 1, 1))}
332
+ className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)]"
333
+ >
334
+ <ChevronLeft className="w-4 h-4" />
335
+ </button>
336
+ <button
337
+ type="button"
338
+ onClick={() => setViewDate(new Date(year, month + 1, 1))}
339
+ className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)]"
340
+ >
341
+ <ChevronRight className="w-4 h-4" />
342
+ </button>
343
+ </div>
344
+ </div>
345
+
346
+ <div className="grid grid-cols-7 gap-1 text-center">
347
+ {Array.from({ length: firstDayIndex }).map((_, i) => (
348
+ <div key={`empty-${i}`} />
349
+ ))}
350
+ {days.map((d, idx) => {
351
+ const dStr = formatDate(d);
352
+ const fromStr = range.from ? formatDate(range.from) : '';
353
+ const toStr = range.to ? formatDate(range.to) : '';
354
+ const isFrom = dStr === fromStr;
355
+ const isTo = dStr === toStr;
356
+ const isInRange =
357
+ range.from && range.to && d > range.from && d < range.to;
358
+
359
+ return (
360
+ <button
361
+ key={idx}
362
+ type="button"
363
+ onClick={() => handleDayClick(d)}
364
+ className={cn(
365
+ 'h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center transition-all cursor-pointer',
366
+ (isFrom || isTo) && 'bg-[var(--ui-primary)] text-white shadow-md',
367
+ isInRange && 'bg-[var(--ui-primary)]/20 text-[var(--ui-primary)] rounded-none',
368
+ !isFrom && !isTo && !isInRange && 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
369
+ )}
370
+ >
371
+ {d.getDate()}
372
+ </button>
373
+ );
374
+ })}
375
+ </div>
376
+ </div>
377
+ </Popover>
378
+ </div>
379
+ );
380
+ }
381
+ );
382
+ DateRangePicker.displayName = 'DateRangePicker';
383
+
384
+ /* -------------------------------------------------------------------------------------------------
385
+ * TimePicker Component
386
+ * -----------------------------------------------------------------------------------------------*/
387
+ export interface TimePickerProps {
388
+ value?: string;
389
+ defaultValue?: string;
390
+ onChange?: (time: string) => void;
391
+ format?: '12h' | '24h';
392
+ className?: string;
393
+ disabled?: boolean;
394
+ }
395
+
396
+ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
397
+ ({ value: controlledVal, defaultValue = '12:00', onChange, format = '24h', className, disabled = false }, ref) => {
398
+ const [internalTime, setInternalTime] = useState(defaultValue);
399
+ const time = controlledVal !== undefined ? controlledVal : internalTime;
400
+
401
+ const [hours, setHours] = useState(time.split(':')[0] || '12');
402
+ const [minutes, setMinutes] = useState(time.split(':')[1] || '00');
403
+ const [isOpen, setIsOpen] = useState(false);
404
+
405
+ const updateTime = (newH: string, newM: string) => {
406
+ const full = `${newH.padStart(2, '0')}:${newM.padStart(2, '0')}`;
407
+ setHours(newH);
408
+ setMinutes(newM);
409
+ setInternalTime(full);
410
+ onChange?.(full);
411
+ };
412
+
413
+ return (
414
+ <div ref={ref} className={cn('relative inline-block w-full', className)}>
415
+ <Popover
416
+ isOpen={isOpen}
417
+ onClose={() => setIsOpen(false)}
418
+ trigger={
419
+ <button
420
+ type="button"
421
+ disabled={disabled}
422
+ onClick={() => !disabled && setIsOpen(!isOpen)}
423
+ className="w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm shadow-inner cursor-pointer"
424
+ >
425
+ <div className="flex items-center gap-2.5">
426
+ <Clock className="w-4 h-4 text-[var(--ui-text-muted)]" />
427
+ <span className="font-mono text-sm font-semibold text-[var(--ui-text-primary)]">{time}</span>
428
+ </div>
429
+ </button>
430
+ }
431
+ >
432
+ <div className="p-4 w-52 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-3">
433
+ <span className="text-xs font-bold text-[var(--ui-text-muted)] uppercase tracking-wider font-mono">
434
+ Select Time ({format})
435
+ </span>
436
+ <div className="flex items-center justify-center gap-2">
437
+ <div className="h-36 overflow-y-auto border border-[var(--ui-border-subtle)] rounded-xl p-1 w-16 text-center space-y-1">
438
+ {Array.from({ length: 24 }).map((_, i) => {
439
+ const h = String(i).padStart(2, '0');
440
+ return (
441
+ <div
442
+ key={h}
443
+ onClick={() => updateTime(h, minutes)}
444
+ className={cn(
445
+ 'text-xs py-1 rounded-lg font-mono font-semibold cursor-pointer transition-colors',
446
+ hours === h
447
+ ? 'bg-[var(--ui-primary)] text-white'
448
+ : 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
449
+ )}
450
+ >
451
+ {h}
452
+ </div>
453
+ );
454
+ })}
455
+ </div>
456
+ <span className="text-sm font-bold">:</span>
457
+ <div className="h-36 overflow-y-auto border border-[var(--ui-border-subtle)] rounded-xl p-1 w-16 text-center space-y-1">
458
+ {['00', '05', '10', '15', '20', '25', '30', '35', '40', '41', '45', '50', '55'].map((m) => (
459
+ <div
460
+ key={m}
461
+ onClick={() => updateTime(hours, m)}
462
+ className={cn(
463
+ 'text-xs py-1 rounded-lg font-mono font-semibold cursor-pointer transition-colors',
464
+ minutes === m
465
+ ? 'bg-[var(--ui-primary)] text-white'
466
+ : 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
467
+ )}
468
+ >
469
+ {m}
470
+ </div>
471
+ ))}
472
+ </div>
473
+ </div>
474
+ </div>
475
+ </Popover>
476
+ </div>
477
+ );
478
+ }
479
+ );
480
+ TimePicker.displayName = 'TimePicker';