@optilogic/core 1.0.0-beta.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/index.cjs +6003 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2310 -0
  6. package/dist/index.d.ts +2310 -0
  7. package/dist/index.js +5828 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.css +96 -0
  10. package/dist/tailwind-preset.cjs +106 -0
  11. package/dist/tailwind-preset.cjs.map +1 -0
  12. package/dist/tailwind-preset.d.cts +23 -0
  13. package/dist/tailwind-preset.d.ts +23 -0
  14. package/dist/tailwind-preset.js +101 -0
  15. package/dist/tailwind-preset.js.map +1 -0
  16. package/package.json +154 -0
  17. package/src/components/accordion.tsx +187 -0
  18. package/src/components/alert-dialog.tsx +143 -0
  19. package/src/components/autocomplete.tsx +271 -0
  20. package/src/components/badge.tsx +62 -0
  21. package/src/components/button.tsx +85 -0
  22. package/src/components/calendar.tsx +235 -0
  23. package/src/components/card.tsx +94 -0
  24. package/src/components/checkbox.tsx +77 -0
  25. package/src/components/chip.tsx +77 -0
  26. package/src/components/confirmation-modal.tsx +195 -0
  27. package/src/components/context-menu.tsx +406 -0
  28. package/src/components/copy-button.tsx +84 -0
  29. package/src/components/data-grid/DataGrid.tsx +1027 -0
  30. package/src/components/data-grid/components/CellEditor.tsx +346 -0
  31. package/src/components/data-grid/components/FilterPopover.tsx +459 -0
  32. package/src/components/data-grid/components/HeaderCell.tsx +207 -0
  33. package/src/components/data-grid/components/index.ts +14 -0
  34. package/src/components/data-grid/hooks/index.ts +28 -0
  35. package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
  36. package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
  37. package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
  38. package/src/components/data-grid/index.ts +71 -0
  39. package/src/components/data-grid/types.ts +478 -0
  40. package/src/components/data-grid/utils/dataProcessing.ts +277 -0
  41. package/src/components/data-grid/utils/index.ts +12 -0
  42. package/src/components/date-picker.tsx +366 -0
  43. package/src/components/dropdown-menu.tsx +230 -0
  44. package/src/components/icon-button.tsx +157 -0
  45. package/src/components/input.tsx +40 -0
  46. package/src/components/label.tsx +37 -0
  47. package/src/components/loading-spinner.tsx +113 -0
  48. package/src/components/modal.tsx +207 -0
  49. package/src/components/popover.tsx +62 -0
  50. package/src/components/progress.tsx +41 -0
  51. package/src/components/resizable-panel.tsx +434 -0
  52. package/src/components/resize-handle.tsx +187 -0
  53. package/src/components/select.tsx +160 -0
  54. package/src/components/separator.tsx +50 -0
  55. package/src/components/skeleton.tsx +37 -0
  56. package/src/components/switch.tsx +59 -0
  57. package/src/components/table.tsx +136 -0
  58. package/src/components/tabs.tsx +102 -0
  59. package/src/components/textarea.tsx +36 -0
  60. package/src/components/theme-picker.tsx +245 -0
  61. package/src/components/toaster.tsx +84 -0
  62. package/src/components/tooltip.tsx +199 -0
  63. package/src/index.ts +318 -0
  64. package/src/styles.css +96 -0
  65. package/src/tailwind-preset.ts +129 -0
  66. package/src/theme/index.ts +41 -0
  67. package/src/theme/presets.ts +502 -0
  68. package/src/theme/types.ts +164 -0
  69. package/src/theme/utils.ts +309 -0
  70. package/src/utils/cn.ts +14 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Data Processing Utilities
3
+ *
4
+ * Functions for sorting and filtering data internally in uncontrolled mode.
5
+ * Also exports helper functions for use in controlled mode.
6
+ */
7
+
8
+ import type {
9
+ ColumnDef,
10
+ SortConfig,
11
+ FilterConfig,
12
+ FilterOperator,
13
+ FilterType,
14
+ CellValue,
15
+ FilterValue,
16
+ } from "../types";
17
+
18
+ /**
19
+ * Get a value from a row using column accessor or key
20
+ */
21
+ export function getCellValue<T>(row: T, column: ColumnDef<T>): CellValue {
22
+ if (column.accessor) {
23
+ return column.accessor(row);
24
+ }
25
+ // Dynamic property access - row is expected to be an object with string keys
26
+ return (row as Record<string, CellValue>)[column.key];
27
+ }
28
+
29
+ /**
30
+ * Apply sorting to data
31
+ */
32
+ export function applySorting<T>(
33
+ data: T[],
34
+ sorting: SortConfig[],
35
+ columns: ColumnDef<T>[]
36
+ ): T[] {
37
+ if (sorting.length === 0) return data;
38
+
39
+ const result = [...data];
40
+
41
+ // Apply sorts in order (primary, secondary, etc.)
42
+ for (const sort of sorting) {
43
+ const column = columns.find((c) => c.key === sort.field);
44
+ if (!column) continue;
45
+
46
+ result.sort((a, b) => {
47
+ // Use custom comparator if provided
48
+ if (column.sortComparator) {
49
+ const cmp = column.sortComparator(a, b);
50
+ return sort.direction === "asc" ? cmp : -cmp;
51
+ }
52
+
53
+ // Default comparison
54
+ const aVal = getCellValue(a, column);
55
+ const bVal = getCellValue(b, column);
56
+
57
+ // Handle null/undefined
58
+ if (aVal == null && bVal == null) return 0;
59
+ if (aVal == null) return sort.direction === "asc" ? 1 : -1;
60
+ if (bVal == null) return sort.direction === "asc" ? -1 : 1;
61
+
62
+ // Compare based on type
63
+ let comparison = 0;
64
+
65
+ if (typeof aVal === "string" && typeof bVal === "string") {
66
+ comparison = aVal.localeCompare(bVal);
67
+ } else if (typeof aVal === "number" && typeof bVal === "number") {
68
+ comparison = aVal - bVal;
69
+ } else if (aVal instanceof Date && bVal instanceof Date) {
70
+ comparison = aVal.getTime() - bVal.getTime();
71
+ } else if (typeof aVal === "boolean" && typeof bVal === "boolean") {
72
+ comparison = aVal === bVal ? 0 : aVal ? 1 : -1;
73
+ } else {
74
+ // Fallback: convert to string
75
+ comparison = String(aVal).localeCompare(String(bVal));
76
+ }
77
+
78
+ return sort.direction === "asc" ? comparison : -comparison;
79
+ });
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Extract a single CellValue from a FilterValue (handles arrays for single-value operations)
87
+ */
88
+ function extractSingleValue(filterValue: FilterValue): CellValue {
89
+ if (Array.isArray(filterValue)) {
90
+ return filterValue[0];
91
+ }
92
+ return filterValue;
93
+ }
94
+
95
+ /**
96
+ * Parse a value as a Date, handling various input formats
97
+ */
98
+ function parseDate(value: CellValue): Date | null {
99
+ if (value == null || value === "") return null;
100
+ if (value instanceof Date) return value;
101
+ if (typeof value === "string" || typeof value === "number") {
102
+ const date = new Date(value);
103
+ return isNaN(date.getTime()) ? null : date;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Compare two dates (ignoring time component for date-only comparisons)
110
+ */
111
+ function compareDates(date1: Date, date2: Date): number {
112
+ // Normalize to start of day for comparison
113
+ const d1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
114
+ const d2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
115
+ return d1.getTime() - d2.getTime();
116
+ }
117
+
118
+ /**
119
+ * Apply a single filter operator to a value.
120
+ * Can be used in controlled mode to filter data with the same logic as internal filtering.
121
+ *
122
+ * @param value - The cell value to test
123
+ * @param filterValue - The filter value to compare against
124
+ * @param filterValueTo - Secondary value for 'between' operator
125
+ * @param operator - The filter operator to apply
126
+ * @param filterType - Optional filter type hint (text, number, date, boolean)
127
+ * @returns true if the value passes the filter, false otherwise
128
+ *
129
+ * @example
130
+ * // Using in controlled mode
131
+ * const filteredData = data.filter(row =>
132
+ * applyFilterOperator(row.name, "john", undefined, "contains", "text")
133
+ * );
134
+ */
135
+ export function applyFilterOperator(
136
+ value: CellValue,
137
+ filterValue: FilterValue,
138
+ filterValueTo: FilterValue,
139
+ operator: FilterOperator,
140
+ filterType?: string
141
+ ): boolean {
142
+ // Handle isEmpty/isNotEmpty first (works for any type)
143
+ if (operator === "isEmpty") {
144
+ return value === "" || value == null;
145
+ }
146
+ if (operator === "isNotEmpty") {
147
+ return value !== "" && value != null;
148
+ }
149
+
150
+ // Handle null/undefined values for other operators
151
+ if (value == null) {
152
+ return false;
153
+ }
154
+
155
+ // Date-specific operators
156
+ if (filterType === "date" || operator === "before" || operator === "after") {
157
+ const dateValue = parseDate(value);
158
+ const dateFilterValue = parseDate(extractSingleValue(filterValue));
159
+ const dateFilterValueTo = parseDate(extractSingleValue(filterValueTo));
160
+
161
+ if (!dateValue) return false;
162
+
163
+ switch (operator) {
164
+ case "equals":
165
+ return dateFilterValue ? compareDates(dateValue, dateFilterValue) === 0 : false;
166
+ case "notEquals":
167
+ return dateFilterValue ? compareDates(dateValue, dateFilterValue) !== 0 : false;
168
+ case "before":
169
+ return dateFilterValue ? compareDates(dateValue, dateFilterValue) < 0 : false;
170
+ case "after":
171
+ return dateFilterValue ? compareDates(dateValue, dateFilterValue) > 0 : false;
172
+ case "between":
173
+ if (!dateFilterValue || !dateFilterValueTo) return false;
174
+ return (
175
+ compareDates(dateValue, dateFilterValue) >= 0 &&
176
+ compareDates(dateValue, dateFilterValueTo) <= 0
177
+ );
178
+ default:
179
+ return true;
180
+ }
181
+ }
182
+
183
+ // Number-specific operators
184
+ if (filterType === "number" || ["gt", "gte", "lt", "lte", "between"].includes(operator)) {
185
+ const numValue = Number(value);
186
+ const singleFilterValue = extractSingleValue(filterValue);
187
+ const numFilterValue = Number(singleFilterValue);
188
+
189
+ if (isNaN(numValue)) return false;
190
+
191
+ switch (operator) {
192
+ case "equals":
193
+ return numValue === numFilterValue;
194
+ case "notEquals":
195
+ return numValue !== numFilterValue;
196
+ case "gt":
197
+ return numValue > numFilterValue;
198
+ case "gte":
199
+ return numValue >= numFilterValue;
200
+ case "lt":
201
+ return numValue < numFilterValue;
202
+ case "lte":
203
+ return numValue <= numFilterValue;
204
+ case "between":
205
+ const singleFilterValueTo = extractSingleValue(filterValueTo);
206
+ const numFilterValueTo = Number(singleFilterValueTo);
207
+ return numValue >= numFilterValue && numValue <= numFilterValueTo;
208
+ default:
209
+ return true;
210
+ }
211
+ }
212
+
213
+ // Boolean filter
214
+ if (filterType === "boolean") {
215
+ const boolValue = value === true || value === "true" || value === 1;
216
+ const singleBoolFilterValue = extractSingleValue(filterValue);
217
+ const boolFilterValue = singleBoolFilterValue === true || singleBoolFilterValue === "true" || singleBoolFilterValue === 1;
218
+ return boolValue === boolFilterValue;
219
+ }
220
+
221
+ // Text/string operators (default)
222
+ const strValue = String(value).toLowerCase();
223
+ const singleStrFilterValue = extractSingleValue(filterValue);
224
+ const strFilterValue = singleStrFilterValue != null ? String(singleStrFilterValue).toLowerCase() : "";
225
+
226
+ switch (operator) {
227
+ case "contains":
228
+ return strValue.includes(strFilterValue);
229
+ case "notContains":
230
+ return !strValue.includes(strFilterValue);
231
+ case "equals":
232
+ return strValue === strFilterValue;
233
+ case "notEquals":
234
+ return strValue !== strFilterValue;
235
+ case "startsWith":
236
+ return strValue.startsWith(strFilterValue);
237
+ case "endsWith":
238
+ return strValue.endsWith(strFilterValue);
239
+ default:
240
+ return true;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Apply filters to data
246
+ */
247
+ export function applyFilters<T>(
248
+ data: T[],
249
+ filters: FilterConfig[],
250
+ columns: ColumnDef<T>[]
251
+ ): T[] {
252
+ if (filters.length === 0) return data;
253
+
254
+ return data.filter((row) => {
255
+ // All filters must pass (AND logic)
256
+ return filters.every((filter) => {
257
+ const column = columns.find((c) => c.key === filter.columnKey);
258
+ if (!column) return true;
259
+
260
+ const value = getCellValue(row, column);
261
+
262
+ // Use custom filter function if provided
263
+ if (column.filterFn) {
264
+ return column.filterFn(row, filter.value, filter.operator);
265
+ }
266
+
267
+ // Use default filter logic with filter type for better handling
268
+ return applyFilterOperator(
269
+ value,
270
+ filter.value,
271
+ filter.valueTo,
272
+ filter.operator,
273
+ column.filterType
274
+ );
275
+ });
276
+ });
277
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * DataGrid Utilities
3
+ *
4
+ * Export data processing utilities
5
+ */
6
+
7
+ export {
8
+ getCellValue,
9
+ applySorting,
10
+ applyFilters,
11
+ applyFilterOperator,
12
+ } from "./dataProcessing";
@@ -0,0 +1,366 @@
1
+ /**
2
+ * DatePicker Component
3
+ *
4
+ * A complete date picker combining an input field with a calendar popover.
5
+ * Built using library primitives for consistent styling.
6
+ *
7
+ * Features:
8
+ * - Input field with formatted date display
9
+ * - Calendar popover for date selection
10
+ * - Controlled and uncontrolled modes
11
+ * - Clearable option
12
+ * - Min/max date constraints
13
+ * - Custom date formatting
14
+ * - Keyboard accessible
15
+ */
16
+
17
+ import * as React from "react";
18
+ import { format, parse, isValid } from "date-fns";
19
+ import { Calendar as CalendarIcon, X } from "lucide-react";
20
+ import { cn } from "../utils/cn";
21
+ import { Button } from "./button";
22
+ import { Calendar } from "./calendar";
23
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
24
+
25
+ export interface DatePickerProps {
26
+ /** Selected date value */
27
+ value?: Date;
28
+ /** Default value for uncontrolled mode */
29
+ defaultValue?: Date;
30
+ /** Callback when date changes */
31
+ onChange?: (date: Date | undefined) => void;
32
+ /** Placeholder text when no date selected */
33
+ placeholder?: string;
34
+ /** Whether the picker is disabled */
35
+ disabled?: boolean;
36
+ /** Minimum selectable date */
37
+ minDate?: Date;
38
+ /** Maximum selectable date */
39
+ maxDate?: Date;
40
+ /** Date format string (date-fns format) */
41
+ dateFormat?: string;
42
+ /** Whether to show a clear button */
43
+ clearable?: boolean;
44
+ /** Additional class name */
45
+ className?: string;
46
+ /** Input ID for label association */
47
+ id?: string;
48
+ /** Input name for form submission */
49
+ name?: string;
50
+ /** Alignment of the popover */
51
+ align?: "start" | "center" | "end";
52
+ /** Side of the popover */
53
+ side?: "top" | "right" | "bottom" | "left";
54
+ /** Whether the popover should match the trigger width */
55
+ matchTriggerWidth?: boolean;
56
+ /** Callback when the popover opens/closes */
57
+ onOpenChange?: (open: boolean) => void;
58
+ }
59
+
60
+ /**
61
+ * DatePicker Component
62
+ *
63
+ * A date input with calendar popover for easy date selection.
64
+ *
65
+ * @example Basic usage
66
+ * ```tsx
67
+ * const [date, setDate] = useState<Date>();
68
+ * <DatePicker value={date} onChange={setDate} />
69
+ * ```
70
+ *
71
+ * @example With constraints
72
+ * ```tsx
73
+ * <DatePicker
74
+ * value={date}
75
+ * onChange={setDate}
76
+ * minDate={new Date()}
77
+ * maxDate={addMonths(new Date(), 3)}
78
+ * placeholder="Select a date"
79
+ * />
80
+ * ```
81
+ *
82
+ * @example Custom format
83
+ * ```tsx
84
+ * <DatePicker
85
+ * value={date}
86
+ * onChange={setDate}
87
+ * dateFormat="dd/MM/yyyy"
88
+ * />
89
+ * ```
90
+ */
91
+ function DatePicker({
92
+ value,
93
+ defaultValue,
94
+ onChange,
95
+ placeholder = "Pick a date",
96
+ disabled = false,
97
+ minDate,
98
+ maxDate,
99
+ dateFormat = "PPP",
100
+ clearable = false,
101
+ className,
102
+ id,
103
+ name,
104
+ align = "start",
105
+ side = "bottom",
106
+ matchTriggerWidth = false,
107
+ onOpenChange,
108
+ }: DatePickerProps) {
109
+ const [open, setOpen] = React.useState(false);
110
+ const [internalValue, setInternalValue] = React.useState<Date | undefined>(
111
+ defaultValue
112
+ );
113
+
114
+ // Controlled vs uncontrolled
115
+ const isControlled = value !== undefined;
116
+ const selectedDate = isControlled ? value : internalValue;
117
+
118
+ const handleSelect = (date: Date | undefined) => {
119
+ if (!isControlled) {
120
+ setInternalValue(date);
121
+ }
122
+ onChange?.(date);
123
+ setOpen(false);
124
+ };
125
+
126
+ const handleClear = (e: React.MouseEvent) => {
127
+ e.stopPropagation();
128
+ handleSelect(undefined);
129
+ };
130
+
131
+ const handleOpenChange = (newOpen: boolean) => {
132
+ setOpen(newOpen);
133
+ onOpenChange?.(newOpen);
134
+ };
135
+
136
+ // Format the selected date for display
137
+ const formattedDate = selectedDate
138
+ ? format(selectedDate, dateFormat)
139
+ : undefined;
140
+
141
+ return (
142
+ <Popover open={open} onOpenChange={handleOpenChange}>
143
+ <PopoverTrigger asChild>
144
+ <Button
145
+ id={id}
146
+ variant="outline"
147
+ disabled={disabled}
148
+ className={cn(
149
+ "w-full justify-start text-left font-normal",
150
+ !selectedDate && "text-muted-foreground",
151
+ className
152
+ )}
153
+ aria-expanded={open}
154
+ aria-haspopup="dialog"
155
+ >
156
+ <CalendarIcon className="mr-2 h-4 w-4 shrink-0" />
157
+ <span className="flex-1 truncate">
158
+ {formattedDate ?? placeholder}
159
+ </span>
160
+ {clearable && selectedDate && (
161
+ <X
162
+ className="ml-2 h-4 w-4 shrink-0 opacity-50 hover:opacity-100"
163
+ onClick={handleClear}
164
+ aria-label="Clear date"
165
+ />
166
+ )}
167
+ </Button>
168
+ </PopoverTrigger>
169
+ <PopoverContent
170
+ className={cn("w-auto p-0", matchTriggerWidth && "w-[var(--radix-popover-trigger-width)]")}
171
+ align={align}
172
+ side={side}
173
+ >
174
+ <Calendar
175
+ mode="single"
176
+ selected={selectedDate}
177
+ onSelect={handleSelect}
178
+ disabled={(date) => {
179
+ if (minDate && date < minDate) return true;
180
+ if (maxDate && date > maxDate) return true;
181
+ return false;
182
+ }}
183
+ initialFocus
184
+ />
185
+ {name && selectedDate && (
186
+ <input
187
+ type="hidden"
188
+ name={name}
189
+ value={format(selectedDate, "yyyy-MM-dd")}
190
+ />
191
+ )}
192
+ </PopoverContent>
193
+ </Popover>
194
+ );
195
+ }
196
+
197
+ DatePicker.displayName = "DatePicker";
198
+
199
+ /**
200
+ * DatePickerInput Component
201
+ *
202
+ * A variant that looks more like a traditional input field.
203
+ * Useful for inline editing or compact layouts.
204
+ */
205
+ export interface DatePickerInputProps extends Omit<DatePickerProps, "className"> {
206
+ /** Additional class name for the input wrapper */
207
+ className?: string;
208
+ /** Size variant */
209
+ size?: "sm" | "default" | "lg";
210
+ }
211
+
212
+ function DatePickerInput({
213
+ value,
214
+ defaultValue,
215
+ onChange,
216
+ placeholder = "Pick a date",
217
+ disabled = false,
218
+ minDate,
219
+ maxDate,
220
+ dateFormat = "yyyy-MM-dd",
221
+ clearable = false,
222
+ className,
223
+ id,
224
+ name,
225
+ align = "start",
226
+ side = "bottom",
227
+ onOpenChange,
228
+ size = "default",
229
+ }: DatePickerInputProps) {
230
+ const [open, setOpen] = React.useState(false);
231
+ const [internalValue, setInternalValue] = React.useState<Date | undefined>(
232
+ defaultValue
233
+ );
234
+ const [inputValue, setInputValue] = React.useState("");
235
+ const inputRef = React.useRef<HTMLInputElement>(null);
236
+
237
+ // Controlled vs uncontrolled
238
+ const isControlled = value !== undefined;
239
+ const selectedDate = isControlled ? value : internalValue;
240
+
241
+ // Sync input value with selected date
242
+ React.useEffect(() => {
243
+ if (selectedDate) {
244
+ setInputValue(format(selectedDate, dateFormat));
245
+ } else {
246
+ setInputValue("");
247
+ }
248
+ }, [selectedDate, dateFormat]);
249
+
250
+ const handleSelect = (date: Date | undefined) => {
251
+ if (!isControlled) {
252
+ setInternalValue(date);
253
+ }
254
+ onChange?.(date);
255
+ setOpen(false);
256
+ };
257
+
258
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
259
+ const newValue = e.target.value;
260
+ setInputValue(newValue);
261
+
262
+ // Try to parse the date
263
+ if (newValue) {
264
+ const parsed = parse(newValue, dateFormat, new Date());
265
+ if (isValid(parsed)) {
266
+ if (!isControlled) {
267
+ setInternalValue(parsed);
268
+ }
269
+ onChange?.(parsed);
270
+ }
271
+ } else {
272
+ handleSelect(undefined);
273
+ }
274
+ };
275
+
276
+ const handleInputBlur = () => {
277
+ // Reset to the valid date on blur if input is invalid
278
+ if (selectedDate) {
279
+ setInputValue(format(selectedDate, dateFormat));
280
+ }
281
+ };
282
+
283
+ const handleClear = (e: React.MouseEvent) => {
284
+ e.stopPropagation();
285
+ handleSelect(undefined);
286
+ setInputValue("");
287
+ };
288
+
289
+ const handleOpenChange = (newOpen: boolean) => {
290
+ setOpen(newOpen);
291
+ onOpenChange?.(newOpen);
292
+ };
293
+
294
+ const sizeClasses = {
295
+ sm: "h-8 text-xs px-2",
296
+ default: "h-9 text-sm px-3",
297
+ lg: "h-10 text-base px-4",
298
+ };
299
+
300
+ return (
301
+ <Popover open={open} onOpenChange={handleOpenChange}>
302
+ <div className={cn("relative", className)}>
303
+ <PopoverTrigger asChild>
304
+ <div
305
+ className={cn(
306
+ "flex items-center rounded-md border border-input bg-background ring-offset-background",
307
+ "focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
308
+ disabled && "opacity-50 cursor-not-allowed",
309
+ sizeClasses[size]
310
+ )}
311
+ >
312
+ <input
313
+ ref={inputRef}
314
+ id={id}
315
+ type="text"
316
+ value={inputValue}
317
+ onChange={handleInputChange}
318
+ onBlur={handleInputBlur}
319
+ placeholder={placeholder}
320
+ disabled={disabled}
321
+ className={cn(
322
+ "flex-1 bg-transparent outline-none placeholder:text-muted-foreground",
323
+ "disabled:cursor-not-allowed"
324
+ )}
325
+ />
326
+ {clearable && selectedDate && (
327
+ <X
328
+ className="h-4 w-4 mx-1 cursor-pointer opacity-50 hover:opacity-100"
329
+ onClick={handleClear}
330
+ aria-label="Clear date"
331
+ />
332
+ )}
333
+ <CalendarIcon
334
+ className="h-4 w-4 opacity-50 cursor-pointer hover:opacity-100"
335
+ onClick={() => !disabled && setOpen(true)}
336
+ />
337
+ </div>
338
+ </PopoverTrigger>
339
+ <PopoverContent className="w-auto p-0" align={align} side={side}>
340
+ <Calendar
341
+ mode="single"
342
+ selected={selectedDate}
343
+ onSelect={handleSelect}
344
+ disabled={(date) => {
345
+ if (minDate && date < minDate) return true;
346
+ if (maxDate && date > maxDate) return true;
347
+ return false;
348
+ }}
349
+ initialFocus
350
+ />
351
+ </PopoverContent>
352
+ {name && selectedDate && (
353
+ <input
354
+ type="hidden"
355
+ name={name}
356
+ value={format(selectedDate, "yyyy-MM-dd")}
357
+ />
358
+ )}
359
+ </div>
360
+ </Popover>
361
+ );
362
+ }
363
+
364
+ DatePickerInput.displayName = "DatePickerInput";
365
+
366
+ export { DatePicker, DatePickerInput };