@neoptocom/neopto-ui 1.5.3 → 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 +398 -85
- package/dist/index.d.cts +52 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +363 -52
- 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/components/Input.tsx +28 -2
- package/src/index.ts +5 -1
- package/src/stories/DateInput.stories.tsx +136 -0
- package/src/stories/Input.stories.tsx +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neoptocom/neopto-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A modern React component library built with Tailwind CSS v4 and TypeScript. Features dark mode, design tokens, and comprehensive Storybook documentation. Requires Tailwind v4+.",
|
|
6
6
|
"keywords": [
|
|
@@ -167,7 +167,7 @@ export default function Autocomplete({
|
|
|
167
167
|
>
|
|
168
168
|
<fieldset
|
|
169
169
|
className={[
|
|
170
|
-
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-
|
|
170
|
+
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-14",
|
|
171
171
|
"border-[var(--border)] focus-within:border-[var(--color-brand)]",
|
|
172
172
|
disabled ? "opacity-60 cursor-not-allowed" : ""
|
|
173
173
|
].join(" ")}
|
|
@@ -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/components/Input.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import Icon from "./Icon";
|
|
2
3
|
|
|
3
4
|
export type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> & {
|
|
4
5
|
/** Input visual variant */
|
|
@@ -11,6 +12,8 @@ export type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size
|
|
|
11
12
|
legendProps?: React.HTMLAttributes<HTMLLegendElement>;
|
|
12
13
|
/** Flag to visually mark the input as errored */
|
|
13
14
|
error?: boolean;
|
|
15
|
+
/** Optional icon name to display on the inner right of the input */
|
|
16
|
+
icon?: string;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
@@ -23,6 +26,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
23
26
|
fieldsetProps,
|
|
24
27
|
legendProps,
|
|
25
28
|
error = false,
|
|
29
|
+
icon,
|
|
26
30
|
...props
|
|
27
31
|
},
|
|
28
32
|
ref
|
|
@@ -30,10 +34,14 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
30
34
|
const isInlineVariant = variant === "inline";
|
|
31
35
|
const shouldUseInlineStyles = isInlineVariant || Boolean(label);
|
|
32
36
|
const isError = error && !disabled;
|
|
37
|
+
const hasIcon = Boolean(icon);
|
|
33
38
|
|
|
34
39
|
const inputClasses: string[] = [
|
|
35
40
|
"w-full bg-transparent outline-none transition-colors",
|
|
36
|
-
shouldUseInlineStyles ? "h-9" : "h-12
|
|
41
|
+
shouldUseInlineStyles ? "h-9" : "h-12 rounded-full",
|
|
42
|
+
shouldUseInlineStyles
|
|
43
|
+
? (hasIcon ? "pr-8" : "")
|
|
44
|
+
: (hasIcon ? "px-4 pr-10" : "px-4"),
|
|
37
45
|
"font-['Poppins'] text-sm placeholder:text-[var(--muted-fg)]"
|
|
38
46
|
];
|
|
39
47
|
|
|
@@ -67,7 +75,18 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
67
75
|
<input ref={ref} disabled={disabled} className={inputClassName} {...props} />
|
|
68
76
|
);
|
|
69
77
|
|
|
78
|
+
// Standalone input (no label)
|
|
70
79
|
if (!label) {
|
|
80
|
+
if (hasIcon) {
|
|
81
|
+
return (
|
|
82
|
+
<div className="relative">
|
|
83
|
+
{inputElement}
|
|
84
|
+
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
85
|
+
<Icon name={icon!} className="text-[var(--muted-fg)] opacity-50" />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
71
90
|
return inputElement;
|
|
72
91
|
}
|
|
73
92
|
|
|
@@ -113,7 +132,14 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
113
132
|
{label}
|
|
114
133
|
</legend>
|
|
115
134
|
<div className="relative flex pl-5 pr-3 pb-1 h-full">
|
|
116
|
-
<div className="flex w-full">
|
|
135
|
+
<div className="flex w-full relative">
|
|
136
|
+
{inputElement}
|
|
137
|
+
{hasIcon && (
|
|
138
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
139
|
+
<Icon name={icon!} className="text-[var(--muted-fg)] opacity-50" />
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
117
143
|
</div>
|
|
118
144
|
</fieldset>
|
|
119
145
|
);
|
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
|
+
|
|
@@ -73,3 +73,14 @@ export const Error: Story = {
|
|
|
73
73
|
</div>
|
|
74
74
|
)
|
|
75
75
|
};
|
|
76
|
+
|
|
77
|
+
export const WithIcon: Story = {
|
|
78
|
+
render: () => (
|
|
79
|
+
<div className="flex flex-col gap-4 w-96">
|
|
80
|
+
<Input variant="inline" icon="search" placeholder="Search..." />
|
|
81
|
+
<Input icon="email" type="email" placeholder="Email" />
|
|
82
|
+
<Input label="Username" icon="person" placeholder="johndoe" />
|
|
83
|
+
<Input label="Password" icon="lock" type="password" placeholder="Required field" error />
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
};
|