@neoptocom/neopto-ui 1.5.4 → 1.6.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.
- package/dist/index.cjs +351 -50
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +316 -17
- package/package.json +1 -1
- package/src/components/Autocomplete.tsx +1 -1
- package/src/components/Calendar.tsx +217 -0
- package/src/components/DateInput.tsx +225 -0
- package/src/index.ts +5 -1
- package/src/stories/DateInput.stories.tsx +136 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type CalendarProps = {
|
|
4
|
+
/** Currently selected date */
|
|
5
|
+
selectedDate?: Date;
|
|
6
|
+
/** Callback when a date is selected */
|
|
7
|
+
onDateSelect: (date: Date) => void;
|
|
8
|
+
/** Today's date (for highlighting) */
|
|
9
|
+
today?: Date;
|
|
10
|
+
/** Minimum selectable date */
|
|
11
|
+
minDate?: Date;
|
|
12
|
+
/** Maximum selectable date */
|
|
13
|
+
maxDate?: Date;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DAYS_OF_WEEK = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
17
|
+
const MONTHS = [
|
|
18
|
+
"January",
|
|
19
|
+
"February",
|
|
20
|
+
"March",
|
|
21
|
+
"April",
|
|
22
|
+
"May",
|
|
23
|
+
"June",
|
|
24
|
+
"July",
|
|
25
|
+
"August",
|
|
26
|
+
"September",
|
|
27
|
+
"October",
|
|
28
|
+
"November",
|
|
29
|
+
"December",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function isSameDay(date1: Date, date2: Date): boolean {
|
|
33
|
+
return (
|
|
34
|
+
date1.getDate() === date2.getDate() &&
|
|
35
|
+
date1.getMonth() === date2.getMonth() &&
|
|
36
|
+
date1.getFullYear() === date2.getFullYear()
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isSameMonth(date1: Date, date2: Date): boolean {
|
|
41
|
+
return (
|
|
42
|
+
date1.getMonth() === date2.getMonth() &&
|
|
43
|
+
date1.getFullYear() === date2.getFullYear()
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function startOfDay(date: Date): Date {
|
|
48
|
+
const d = new Date(date);
|
|
49
|
+
d.setHours(0, 0, 0, 0);
|
|
50
|
+
return d;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function Calendar({
|
|
54
|
+
selectedDate,
|
|
55
|
+
onDateSelect,
|
|
56
|
+
today = new Date(),
|
|
57
|
+
minDate,
|
|
58
|
+
maxDate,
|
|
59
|
+
}: CalendarProps) {
|
|
60
|
+
const [currentMonth, setCurrentMonth] = React.useState(
|
|
61
|
+
selectedDate || new Date()
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const todayStart = startOfDay(today);
|
|
65
|
+
const selectedDateStart = selectedDate ? startOfDay(selectedDate) : null;
|
|
66
|
+
|
|
67
|
+
const firstDayOfMonth = new Date(
|
|
68
|
+
currentMonth.getFullYear(),
|
|
69
|
+
currentMonth.getMonth(),
|
|
70
|
+
1
|
|
71
|
+
);
|
|
72
|
+
const lastDayOfMonth = new Date(
|
|
73
|
+
currentMonth.getFullYear(),
|
|
74
|
+
currentMonth.getMonth() + 1,
|
|
75
|
+
0
|
|
76
|
+
);
|
|
77
|
+
const daysInMonth = lastDayOfMonth.getDate();
|
|
78
|
+
const startingDayOfWeek = firstDayOfMonth.getDay();
|
|
79
|
+
|
|
80
|
+
const prevMonth = () => {
|
|
81
|
+
setCurrentMonth(
|
|
82
|
+
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const nextMonth = () => {
|
|
87
|
+
setCurrentMonth(
|
|
88
|
+
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleDateClick = (day: number) => {
|
|
93
|
+
const date = new Date(
|
|
94
|
+
currentMonth.getFullYear(),
|
|
95
|
+
currentMonth.getMonth(),
|
|
96
|
+
day
|
|
97
|
+
);
|
|
98
|
+
const dateStart = startOfDay(date);
|
|
99
|
+
|
|
100
|
+
if (minDate && dateStart < startOfDay(minDate)) return;
|
|
101
|
+
if (maxDate && dateStart > startOfDay(maxDate)) return;
|
|
102
|
+
|
|
103
|
+
onDateSelect(date);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const isDateDisabled = (day: number): boolean => {
|
|
107
|
+
const date = new Date(
|
|
108
|
+
currentMonth.getFullYear(),
|
|
109
|
+
currentMonth.getMonth(),
|
|
110
|
+
day
|
|
111
|
+
);
|
|
112
|
+
const dateStart = startOfDay(date);
|
|
113
|
+
|
|
114
|
+
if (minDate && dateStart < startOfDay(minDate)) return true;
|
|
115
|
+
if (maxDate && dateStart > startOfDay(maxDate)) return true;
|
|
116
|
+
return false;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const days = [];
|
|
120
|
+
// Empty cells for days before the first day of the month
|
|
121
|
+
for (let i = 0; i < startingDayOfWeek; i++) {
|
|
122
|
+
days.push(null);
|
|
123
|
+
}
|
|
124
|
+
// Days of the month
|
|
125
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
126
|
+
days.push(day);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="w-full">
|
|
131
|
+
{/* Header */}
|
|
132
|
+
<div className="flex items-center justify-between mb-4">
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={prevMonth}
|
|
136
|
+
className="p-2 rounded-full hover:bg-[var(--muted)] transition-colors"
|
|
137
|
+
aria-label="Previous month"
|
|
138
|
+
>
|
|
139
|
+
<span className="material-symbols-rounded text-[var(--fg)]">
|
|
140
|
+
chevron_left
|
|
141
|
+
</span>
|
|
142
|
+
</button>
|
|
143
|
+
<h3 className="text-sm font-medium text-[var(--fg)]">
|
|
144
|
+
{MONTHS[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
|
145
|
+
</h3>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={nextMonth}
|
|
149
|
+
className="p-2 rounded-full hover:bg-[var(--muted)] transition-colors"
|
|
150
|
+
aria-label="Next month"
|
|
151
|
+
>
|
|
152
|
+
<span className="material-symbols-rounded text-[var(--fg)]">
|
|
153
|
+
chevron_right
|
|
154
|
+
</span>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Days of week header */}
|
|
159
|
+
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
160
|
+
{DAYS_OF_WEEK.map((day) => (
|
|
161
|
+
<div
|
|
162
|
+
key={day}
|
|
163
|
+
className="text-xs text-center text-[var(--muted-fg)] font-medium py-1"
|
|
164
|
+
>
|
|
165
|
+
{day}
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Calendar grid */}
|
|
171
|
+
<div className="grid grid-cols-7 gap-1">
|
|
172
|
+
{days.map((day, idx) => {
|
|
173
|
+
if (day === null) {
|
|
174
|
+
return <div key={`empty-${idx}`} className="aspect-square" />;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const date = new Date(
|
|
178
|
+
currentMonth.getFullYear(),
|
|
179
|
+
currentMonth.getMonth(),
|
|
180
|
+
day
|
|
181
|
+
);
|
|
182
|
+
const dateStart = startOfDay(date);
|
|
183
|
+
const isSelected = selectedDateStart && isSameDay(dateStart, selectedDateStart);
|
|
184
|
+
const isToday = isSameDay(dateStart, todayStart);
|
|
185
|
+
const isDisabled = isDateDisabled(day);
|
|
186
|
+
const isCurrentMonth = isSameMonth(dateStart, currentMonth);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<button
|
|
190
|
+
key={day}
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={() => handleDateClick(day)}
|
|
193
|
+
disabled={isDisabled}
|
|
194
|
+
className={[
|
|
195
|
+
"aspect-square rounded-lg text-sm transition-colors",
|
|
196
|
+
isSelected
|
|
197
|
+
? "bg-[var(--color-brand)] text-white font-medium"
|
|
198
|
+
: isToday
|
|
199
|
+
? "bg-[var(--muted)] text-[var(--fg)] font-medium border border-[var(--color-brand)]"
|
|
200
|
+
: "text-[var(--fg)] hover:bg-[var(--muted)]",
|
|
201
|
+
isDisabled
|
|
202
|
+
? "opacity-30 cursor-not-allowed"
|
|
203
|
+
: "cursor-pointer",
|
|
204
|
+
!isCurrentMonth ? "opacity-50" : "",
|
|
205
|
+
]
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.join(" ")}
|
|
208
|
+
>
|
|
209
|
+
{day}
|
|
210
|
+
</button>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Input } from "./Input";
|
|
3
|
+
import Calendar from "./Calendar";
|
|
4
|
+
import { Card } from "./Card";
|
|
5
|
+
|
|
6
|
+
export type DateInputProps = Omit<
|
|
7
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
8
|
+
"value" | "onChange" | "type"
|
|
9
|
+
> & {
|
|
10
|
+
/** Label text displayed above the input */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Current date value */
|
|
13
|
+
value?: Date | null;
|
|
14
|
+
/** Callback when date changes */
|
|
15
|
+
onChange: (date: Date) => void;
|
|
16
|
+
/** Flag to visually mark the input as errored */
|
|
17
|
+
error?: boolean;
|
|
18
|
+
/** Minimum selectable date */
|
|
19
|
+
minDate?: Date;
|
|
20
|
+
/** Maximum selectable date */
|
|
21
|
+
maxDate?: Date;
|
|
22
|
+
/** Placeholder text (default: "00/00/0000") */
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function formatDate(date: Date): string {
|
|
27
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
28
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
29
|
+
const year = date.getFullYear();
|
|
30
|
+
return `${day}/${month}/${year}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseDate(dateString: string): Date | null {
|
|
34
|
+
const parts = dateString.split("/");
|
|
35
|
+
if (parts.length !== 3) return null;
|
|
36
|
+
|
|
37
|
+
const day = parseInt(parts[0], 10);
|
|
38
|
+
const month = parseInt(parts[1], 10) - 1; // Month is 0-indexed
|
|
39
|
+
const year = parseInt(parts[2], 10);
|
|
40
|
+
|
|
41
|
+
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
|
|
42
|
+
|
|
43
|
+
const date = new Date(year, month, day);
|
|
44
|
+
// Check if date is valid
|
|
45
|
+
if (
|
|
46
|
+
date.getDate() !== day ||
|
|
47
|
+
date.getMonth() !== month ||
|
|
48
|
+
date.getFullYear() !== year
|
|
49
|
+
) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return date;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isValidDate(date: Date): boolean {
|
|
57
|
+
return date instanceof Date && !isNaN(date.getTime());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function startOfDay(date: Date): Date {
|
|
61
|
+
const d = new Date(date);
|
|
62
|
+
d.setHours(0, 0, 0, 0);
|
|
63
|
+
return d;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
|
|
67
|
+
(
|
|
68
|
+
{
|
|
69
|
+
label,
|
|
70
|
+
value,
|
|
71
|
+
onChange,
|
|
72
|
+
error = false,
|
|
73
|
+
disabled = false,
|
|
74
|
+
minDate,
|
|
75
|
+
maxDate,
|
|
76
|
+
placeholder = "00/00/0000",
|
|
77
|
+
className = "",
|
|
78
|
+
...props
|
|
79
|
+
},
|
|
80
|
+
ref
|
|
81
|
+
) => {
|
|
82
|
+
const [inputValue, setInputValue] = React.useState(
|
|
83
|
+
value && isValidDate(value) ? formatDate(value) : placeholder
|
|
84
|
+
);
|
|
85
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
86
|
+
const [showCalendar, setShowCalendar] = React.useState(false);
|
|
87
|
+
const [initialDateSet, setInitialDateSet] = React.useState(true);
|
|
88
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
89
|
+
|
|
90
|
+
// Update input value when value prop changes
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
if (value && isValidDate(value)) {
|
|
93
|
+
setInputValue(formatDate(value));
|
|
94
|
+
} else {
|
|
95
|
+
setInputValue(placeholder);
|
|
96
|
+
}
|
|
97
|
+
}, [value, placeholder]);
|
|
98
|
+
|
|
99
|
+
// Set today's date when calendar first opens
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (showCalendar && initialDateSet) {
|
|
102
|
+
const today = new Date();
|
|
103
|
+
onChange(today);
|
|
104
|
+
setInputValue(formatDate(today));
|
|
105
|
+
setInitialDateSet(false);
|
|
106
|
+
}
|
|
107
|
+
}, [showCalendar, initialDateSet, onChange]);
|
|
108
|
+
|
|
109
|
+
// Handle click outside
|
|
110
|
+
React.useEffect(() => {
|
|
111
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
112
|
+
if (
|
|
113
|
+
containerRef.current &&
|
|
114
|
+
!containerRef.current.contains(event.target as Node)
|
|
115
|
+
) {
|
|
116
|
+
setShowCalendar(false);
|
|
117
|
+
setInitialDateSet(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
122
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
126
|
+
let rawValue = e.target.value;
|
|
127
|
+
|
|
128
|
+
// Remove non-digits and format as dd/MM/yyyy
|
|
129
|
+
rawValue = rawValue
|
|
130
|
+
.replace(/\D/g, "")
|
|
131
|
+
.replace(/^(\d{2})/, "$1/")
|
|
132
|
+
.replace(/^(\d{2}\/\d{2})/, "$1/")
|
|
133
|
+
.slice(0, 10);
|
|
134
|
+
|
|
135
|
+
setInputValue(rawValue);
|
|
136
|
+
|
|
137
|
+
// Parse and validate when complete
|
|
138
|
+
if (rawValue.length === 10) {
|
|
139
|
+
const parsedDate = parseDate(rawValue);
|
|
140
|
+
if (parsedDate && isValidDate(parsedDate)) {
|
|
141
|
+
onChange(parsedDate);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleCalendarSelect = (date: Date) => {
|
|
147
|
+
const selectedDate = parseDate(inputValue);
|
|
148
|
+
const sameDay =
|
|
149
|
+
selectedDate &&
|
|
150
|
+
isValidDate(selectedDate) &&
|
|
151
|
+
selectedDate.getDate() === date.getDate() &&
|
|
152
|
+
selectedDate.getMonth() === date.getMonth() &&
|
|
153
|
+
selectedDate.getFullYear() === date.getFullYear();
|
|
154
|
+
|
|
155
|
+
if (!sameDay) {
|
|
156
|
+
onChange(date);
|
|
157
|
+
setInputValue(formatDate(date));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setInitialDateSet(false);
|
|
161
|
+
setShowCalendar(false);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleInputFocus = () => {
|
|
165
|
+
setIsFocused(true);
|
|
166
|
+
setShowCalendar(true);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleInputBlur = () => {
|
|
170
|
+
setIsFocused(false);
|
|
171
|
+
const parsed = parseDate(inputValue);
|
|
172
|
+
if (!parsed || !isValidDate(parsed)) {
|
|
173
|
+
const today = new Date();
|
|
174
|
+
onChange(today);
|
|
175
|
+
setInputValue(formatDate(today));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isEmpty = inputValue === placeholder;
|
|
180
|
+
const textColorClass = isEmpty ? "text-[var(--muted-fg)]" : "";
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className={["relative w-full", className].join(" ")} ref={containerRef}>
|
|
184
|
+
<Input
|
|
185
|
+
ref={ref}
|
|
186
|
+
label={label}
|
|
187
|
+
type="text"
|
|
188
|
+
value={inputValue}
|
|
189
|
+
onChange={handleInputChange}
|
|
190
|
+
onFocus={handleInputFocus}
|
|
191
|
+
onBlur={handleInputBlur}
|
|
192
|
+
onClick={() => !disabled && setShowCalendar(true)}
|
|
193
|
+
disabled={disabled}
|
|
194
|
+
error={error}
|
|
195
|
+
icon="calendar_today"
|
|
196
|
+
className={textColorClass}
|
|
197
|
+
{...props}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
{showCalendar && !disabled && (
|
|
201
|
+
<div className="absolute z-20 mt-2 w-full max-w-sm">
|
|
202
|
+
<Card className="p-4" showDecorations={false}>
|
|
203
|
+
<Calendar
|
|
204
|
+
selectedDate={
|
|
205
|
+
inputValue !== placeholder &&
|
|
206
|
+
parseDate(inputValue) &&
|
|
207
|
+
isValidDate(parseDate(inputValue)!)
|
|
208
|
+
? parseDate(inputValue)!
|
|
209
|
+
: new Date()
|
|
210
|
+
}
|
|
211
|
+
onDateSelect={handleCalendarSelect}
|
|
212
|
+
today={startOfDay(new Date())}
|
|
213
|
+
minDate={minDate}
|
|
214
|
+
maxDate={maxDate}
|
|
215
|
+
/>
|
|
216
|
+
</Card>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
DateInput.displayName = "DateInput";
|
|
225
|
+
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,8 @@ export * from "./components/Chat";
|
|
|
23
23
|
export * from "./components/MessageBubble";
|
|
24
24
|
export * from "./components/Separator";
|
|
25
25
|
export { Breadcrumb } from "./components/Breadcrumb";
|
|
26
|
+
export { DateInput } from "./components/DateInput";
|
|
27
|
+
export { default as Calendar } from "./components/Calendar";
|
|
26
28
|
|
|
27
29
|
// Types
|
|
28
30
|
export type { AppBackgroundProps } from "./components/AppBackground";
|
|
@@ -45,4 +47,6 @@ export type { CounterProps } from "./components/Counter";
|
|
|
45
47
|
export type { AgentButtonProps } from "./components/Chat";
|
|
46
48
|
export type { MessageBubbleProps } from "./components/MessageBubble";
|
|
47
49
|
export type { SeparatorProps } from "./components/Separator";
|
|
48
|
-
export type { BreadcrumbProps, BreadcrumbItem } from "./components/Breadcrumb";
|
|
50
|
+
export type { BreadcrumbProps, BreadcrumbItem } from "./components/Breadcrumb";
|
|
51
|
+
export type { DateInputProps } from "./components/DateInput";
|
|
52
|
+
export type { CalendarProps } from "./components/Calendar";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { DateInput } from "../components/DateInput";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DateInput> = {
|
|
6
|
+
title: "Components/DateInput",
|
|
7
|
+
component: DateInput
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof DateInput>;
|
|
11
|
+
|
|
12
|
+
export const Default: Story = {
|
|
13
|
+
render: () => {
|
|
14
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-col gap-4 w-96">
|
|
17
|
+
<DateInput
|
|
18
|
+
label="Select date"
|
|
19
|
+
value={date}
|
|
20
|
+
onChange={(d) => setDate(d)}
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const WithInitialValue: Story = {
|
|
28
|
+
render: () => {
|
|
29
|
+
const [date, setDate] = useState<Date | null>(new Date());
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-col gap-4 w-96">
|
|
32
|
+
<DateInput
|
|
33
|
+
label="Date"
|
|
34
|
+
value={date}
|
|
35
|
+
onChange={(d) => setDate(d)}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const WithoutLabel: Story = {
|
|
43
|
+
render: () => {
|
|
44
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col gap-4 w-96">
|
|
47
|
+
<DateInput
|
|
48
|
+
value={date}
|
|
49
|
+
onChange={(d) => setDate(d)}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Disabled: Story = {
|
|
57
|
+
render: () => {
|
|
58
|
+
const [date, setDate] = useState<Date | null>(new Date());
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex flex-col gap-4 w-96">
|
|
61
|
+
<DateInput
|
|
62
|
+
label="Disabled date"
|
|
63
|
+
value={date}
|
|
64
|
+
onChange={(d) => setDate(d)}
|
|
65
|
+
disabled
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const WithError: Story = {
|
|
73
|
+
render: () => {
|
|
74
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex flex-col gap-4 w-96">
|
|
77
|
+
<DateInput
|
|
78
|
+
label="Date"
|
|
79
|
+
value={date}
|
|
80
|
+
onChange={(d) => setDate(d)}
|
|
81
|
+
error
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const WithMinMax: Story = {
|
|
89
|
+
render: () => {
|
|
90
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
91
|
+
const today = new Date();
|
|
92
|
+
const minDate = new Date(today);
|
|
93
|
+
minDate.setDate(today.getDate() - 7); // 7 days ago
|
|
94
|
+
const maxDate = new Date(today);
|
|
95
|
+
maxDate.setDate(today.getDate() + 30); // 30 days from now
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex flex-col gap-4 w-96">
|
|
99
|
+
<DateInput
|
|
100
|
+
label="Select date (within range)"
|
|
101
|
+
value={date}
|
|
102
|
+
onChange={(d) => setDate(d)}
|
|
103
|
+
minDate={minDate}
|
|
104
|
+
maxDate={maxDate}
|
|
105
|
+
/>
|
|
106
|
+
<p className="text-xs text-[var(--muted-fg)]">
|
|
107
|
+
Available range: {minDate.toLocaleDateString()} to {maxDate.toLocaleDateString()}
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Multiple: Story = {
|
|
115
|
+
render: () => {
|
|
116
|
+
const [startDate, setStartDate] = useState<Date | null>(null);
|
|
117
|
+
const [endDate, setEndDate] = useState<Date | null>(null);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex flex-col gap-4 w-96">
|
|
121
|
+
<DateInput
|
|
122
|
+
label="Start date"
|
|
123
|
+
value={startDate}
|
|
124
|
+
onChange={(d) => setStartDate(d)}
|
|
125
|
+
/>
|
|
126
|
+
<DateInput
|
|
127
|
+
label="End date"
|
|
128
|
+
value={endDate}
|
|
129
|
+
onChange={(d) => setEndDate(d)}
|
|
130
|
+
minDate={startDate || undefined}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|