@idealyst/datepicker 1.1.6 → 1.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/datepicker",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Cross-platform date and time picker components for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/datepicker#readme",
6
6
  "readme": "README.md",
@@ -36,8 +36,8 @@
36
36
  "publish:npm": "npm publish"
37
37
  },
38
38
  "peerDependencies": {
39
- "@idealyst/components": "^1.1.6",
40
- "@idealyst/theme": "^1.1.6",
39
+ "@idealyst/components": "^1.1.7",
40
+ "@idealyst/theme": "^1.1.7",
41
41
  "react": ">=16.8.0",
42
42
  "react-native": ">=0.60.0",
43
43
  "react-native-svg": ">=13.0.0",
@@ -61,8 +61,8 @@
61
61
  }
62
62
  },
63
63
  "devDependencies": {
64
- "@idealyst/components": "^1.1.6",
65
- "@idealyst/theme": "^1.1.6",
64
+ "@idealyst/components": "^1.1.7",
65
+ "@idealyst/theme": "^1.1.7",
66
66
  "@types/react": "^19.1.0",
67
67
  "react": "^19.1.0",
68
68
  "react-native": "^0.80.1",
@@ -69,38 +69,27 @@ export const DateInput: React.FC<DateInputProps> = ({
69
69
  setOpen(false);
70
70
  };
71
71
 
72
+ // Apply variants for input container
73
+ datePickerStyles.useVariants({
74
+ disabled,
75
+ error: !!error,
76
+ });
77
+
72
78
  return (
73
79
  <View style={style}>
74
80
  {label && (
75
- <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
81
+ <Text typography="body2" weight="medium" style={datePickerStyles.labelText}>
76
82
  {label}
77
83
  </Text>
78
84
  )}
79
- <View
80
- style={{
81
- flexDirection: 'row',
82
- alignItems: 'center',
83
- borderWidth: 1,
84
- borderColor: error ? '#ef4444' : '#d1d5db',
85
- borderRadius: 6,
86
- backgroundColor: disabled ? '#f3f4f6' : 'white',
87
- overflow: 'hidden',
88
- }}
89
- >
85
+ <View style={datePickerStyles.inputContainer}>
90
86
  <RNTextInput
91
87
  value={inputValue}
92
88
  onChangeText={handleInputChange}
93
89
  onBlur={handleInputBlur}
94
90
  placeholder={placeholder}
95
91
  editable={!disabled}
96
- style={{
97
- flex: 1,
98
- padding: 8,
99
- paddingHorizontal: 12,
100
- fontSize: 14,
101
- backgroundColor: 'transparent',
102
- color: disabled ? '#9ca3af' : '#111827',
103
- }}
92
+ style={datePickerStyles.textInput}
104
93
  />
105
94
  <Button
106
95
  type="text"
@@ -113,7 +102,7 @@ export const DateInput: React.FC<DateInputProps> = ({
113
102
  </Button>
114
103
  </View>
115
104
  {error && (
116
- <Text typography="caption" style={{ marginTop: 4, color: '#ef4444' }}>
105
+ <Text typography="caption" style={datePickerStyles.errorText}>
117
106
  {error}
118
107
  </Text>
119
108
  )}
@@ -124,14 +113,7 @@ export const DateInput: React.FC<DateInputProps> = ({
124
113
  animationType="fade"
125
114
  onRequestClose={() => setOpen(false)}
126
115
  >
127
- <View
128
- style={{
129
- flex: 1,
130
- justifyContent: 'center',
131
- alignItems: 'center',
132
- backgroundColor: 'rgba(0,0,0,0.5)',
133
- }}
134
- >
116
+ <View style={datePickerStyles.modalBackdrop}>
135
117
  <View style={datePickerStyles.popoverContent}>
136
118
  <DatePicker
137
119
  value={value ?? undefined}
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useRef, useEffect } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
2
3
  import { View, Text, Button, Icon } from '@idealyst/components';
3
4
  import { PositionedPortal } from '@idealyst/components/internal';
4
5
  import { DatePicker } from './DatePicker';
@@ -71,24 +72,24 @@ export const DateInput: React.FC<DateInputProps> = ({
71
72
  setOpen(false);
72
73
  };
73
74
 
75
+ // Apply variants for input container
76
+ datePickerStyles.useVariants({
77
+ disabled,
78
+ error: !!error,
79
+ });
80
+
81
+ // Get web props for styled elements
82
+ const containerProps = getWebProps([datePickerStyles.inputContainer]);
83
+ const inputProps = getWebProps([datePickerStyles.textInput]);
84
+
74
85
  return (
75
86
  <View style={style}>
76
87
  {label && (
77
- <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
88
+ <Text typography="body2" weight="medium" style={datePickerStyles.labelText}>
78
89
  {label}
79
90
  </Text>
80
91
  )}
81
- <div
82
- ref={triggerRef}
83
- style={{
84
- display: 'flex',
85
- alignItems: 'center',
86
- border: `1px solid ${error ? '#ef4444' : '#d1d5db'}`,
87
- borderRadius: 6,
88
- backgroundColor: disabled ? '#f3f4f6' : 'white',
89
- overflow: 'hidden',
90
- }}
91
- >
92
+ <div ref={triggerRef} {...containerProps}>
92
93
  <input
93
94
  type="text"
94
95
  value={inputValue}
@@ -96,15 +97,7 @@ export const DateInput: React.FC<DateInputProps> = ({
96
97
  onBlur={handleInputBlur}
97
98
  placeholder={placeholder}
98
99
  disabled={disabled}
99
- style={{
100
- flex: 1,
101
- padding: '8px 12px',
102
- fontSize: 14,
103
- border: 'none',
104
- outline: 'none',
105
- backgroundColor: 'transparent',
106
- color: disabled ? '#9ca3af' : '#111827',
107
- }}
100
+ {...inputProps}
108
101
  />
109
102
  <Button
110
103
  type="text"
@@ -117,7 +110,7 @@ export const DateInput: React.FC<DateInputProps> = ({
117
110
  </Button>
118
111
  </div>
119
112
  {error && (
120
- <Text typography="caption" style={{ marginTop: 4, color: '#ef4444' }}>
113
+ <Text typography="caption" style={datePickerStyles.errorText}>
121
114
  {error}
122
115
  </Text>
123
116
  )}
@@ -1,5 +1,6 @@
1
1
  import React, { useMemo, useState } from 'react';
2
- import { View, Text, Button, Icon } from '@idealyst/components';
2
+ import { View } from 'react-native';
3
+ import { Text, Button, Icon } from '@idealyst/components';
3
4
  import { datePickerStyles } from './styles';
4
5
  import type { DatePickerProps } from './types';
5
6
 
@@ -21,7 +22,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
21
22
 
22
23
  datePickerStyles.useVariants({ disabled });
23
24
 
24
- const { days, monthLabel, monthShort, year } = useMemo(() => {
25
+ const { days, monthShort, year } = useMemo(() => {
25
26
  const year = currentMonth.getFullYear();
26
27
  const month = currentMonth.getMonth();
27
28
  const firstDay = new Date(year, month, 1);
@@ -53,12 +54,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
53
54
  }
54
55
  }
55
56
 
56
- const monthLabel = firstDay.toLocaleDateString('en-US', {
57
- month: 'short',
58
- year: 'numeric',
59
- });
60
-
61
- return { days, monthLabel, monthShort: MONTHS[month], year };
57
+ return { days, monthShort: MONTHS[month], year };
62
58
  }, [currentMonth]);
63
59
 
64
60
  const isSelected = (date: Date): boolean => {
@@ -240,16 +236,9 @@ export const DatePicker: React.FC<DatePickerProps> = ({
240
236
  key={index}
241
237
  style={[
242
238
  datePickerStyles.dayCell,
243
- selected && {
244
- backgroundColor: '#3b82f6',
245
- borderRadius: 4,
246
- },
239
+ selected && datePickerStyles.selectedDay,
247
240
  !isCurrentMonth && { opacity: 0.3 },
248
- today && !selected && {
249
- borderWidth: 1,
250
- borderColor: '#3b82f6',
251
- borderRadius: 4,
252
- },
241
+ today && !selected && datePickerStyles.todayDay,
253
242
  dayDisabled && { opacity: 0.3 },
254
243
  ]}
255
244
  >
@@ -0,0 +1,279 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { Text, Button, Icon } from '@idealyst/components';
4
+ import { datePickerStyles } from './styles';
5
+ import type { DatePickerProps } from './types';
6
+
7
+ const WEEKDAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
8
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
9
+
10
+ type ViewMode = 'calendar' | 'months' | 'years';
11
+
12
+ export const DatePicker: React.FC<DatePickerProps> = ({
13
+ value,
14
+ onChange,
15
+ minDate,
16
+ maxDate,
17
+ disabled = false,
18
+ style,
19
+ }) => {
20
+ const [currentMonth, setCurrentMonth] = useState(() => value || new Date());
21
+ const [viewMode, setViewMode] = useState<ViewMode>('calendar');
22
+
23
+ datePickerStyles.useVariants({ disabled });
24
+
25
+ const { days, monthShort, year } = useMemo(() => {
26
+ const year = currentMonth.getFullYear();
27
+ const month = currentMonth.getMonth();
28
+ const firstDay = new Date(year, month, 1);
29
+ const lastDay = new Date(year, month + 1, 0);
30
+ const startPadding = firstDay.getDay();
31
+ const daysInMonth = lastDay.getDate();
32
+
33
+ const days: Array<{ date: Date; isCurrentMonth: boolean }> = [];
34
+
35
+ // Previous month padding
36
+ const prevMonthEnd = new Date(year, month, 0);
37
+ for (let i = startPadding - 1; i >= 0; i--) {
38
+ days.push({
39
+ date: new Date(year, month - 1, prevMonthEnd.getDate() - i),
40
+ isCurrentMonth: false,
41
+ });
42
+ }
43
+
44
+ // Current month
45
+ for (let day = 1; day <= daysInMonth; day++) {
46
+ days.push({ date: new Date(year, month, day), isCurrentMonth: true });
47
+ }
48
+
49
+ // Next month padding (fill to complete last week)
50
+ const remaining = 7 - (days.length % 7);
51
+ if (remaining < 7) {
52
+ for (let day = 1; day <= remaining; day++) {
53
+ days.push({ date: new Date(year, month + 1, day), isCurrentMonth: false });
54
+ }
55
+ }
56
+
57
+ return { days, monthShort: MONTHS[month], year };
58
+ }, [currentMonth]);
59
+
60
+ const isSelected = (date: Date): boolean => {
61
+ if (!value) return false;
62
+ return (
63
+ date.getDate() === value.getDate() &&
64
+ date.getMonth() === value.getMonth() &&
65
+ date.getFullYear() === value.getFullYear()
66
+ );
67
+ };
68
+
69
+ const isToday = (date: Date): boolean => {
70
+ const today = new Date();
71
+ return (
72
+ date.getDate() === today.getDate() &&
73
+ date.getMonth() === today.getMonth() &&
74
+ date.getFullYear() === today.getFullYear()
75
+ );
76
+ };
77
+
78
+ const isDisabled = (date: Date): boolean => {
79
+ if (disabled) return true;
80
+ if (minDate && date < new Date(minDate.setHours(0, 0, 0, 0))) return true;
81
+ if (maxDate && date > new Date(maxDate.setHours(23, 59, 59, 999))) return true;
82
+ return false;
83
+ };
84
+
85
+ const goToPrevMonth = () => {
86
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1));
87
+ };
88
+
89
+ const goToNextMonth = () => {
90
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1));
91
+ };
92
+
93
+ const handleDayPress = (date: Date) => {
94
+ if (!isDisabled(date)) {
95
+ onChange(date);
96
+ }
97
+ };
98
+
99
+ const handleMonthSelect = (monthIndex: number) => {
100
+ setCurrentMonth(new Date(currentMonth.getFullYear(), monthIndex, 1));
101
+ setViewMode('calendar');
102
+ };
103
+
104
+ const handleYearSelect = (selectedYear: number) => {
105
+ setCurrentMonth(new Date(selectedYear, currentMonth.getMonth(), 1));
106
+ setViewMode('months');
107
+ };
108
+
109
+ // Generate year range (10 years before and after current)
110
+ const yearRange = useMemo(() => {
111
+ const currentYear = currentMonth.getFullYear();
112
+ const startYear = Math.floor(currentYear / 10) * 10 - 10;
113
+ return Array.from({ length: 21 }, (_, i) => startYear + i);
114
+ }, [currentMonth]);
115
+
116
+ const goToPrevYearRange = () => {
117
+ setCurrentMonth(new Date(currentMonth.getFullYear() - 10, currentMonth.getMonth(), 1));
118
+ };
119
+
120
+ const goToNextYearRange = () => {
121
+ setCurrentMonth(new Date(currentMonth.getFullYear() + 10, currentMonth.getMonth(), 1));
122
+ };
123
+
124
+ // Get web props for styled elements
125
+ const calendarProps = getWebProps([datePickerStyles.calendar, style as any]);
126
+ const headerProps = getWebProps([datePickerStyles.calendarHeader]);
127
+ const weekdayRowProps = getWebProps([datePickerStyles.weekdayRow]);
128
+ const weekdayCellProps = getWebProps([datePickerStyles.weekdayCell]);
129
+ const gridProps = getWebProps([datePickerStyles.calendarGrid]);
130
+ const monthGridProps = getWebProps([datePickerStyles.monthGrid]);
131
+ const yearGridProps = getWebProps([datePickerStyles.yearGrid]);
132
+
133
+ // Render month selector
134
+ if (viewMode === 'months') {
135
+ return (
136
+ <div {...calendarProps}>
137
+ <div {...headerProps}>
138
+ <Button type="text" size="sm" onPress={() => setViewMode('calendar')} disabled={disabled}>
139
+ <Icon name="chevron-left" size={16} />
140
+ </Button>
141
+ <Button type="text" size="sm" onPress={() => setViewMode('years')} disabled={disabled}>
142
+ <Text typography="body2" weight="semibold">{year}</Text>
143
+ </Button>
144
+ <div style={{ width: 28 }} />
145
+ </div>
146
+ <div {...monthGridProps}>
147
+ {MONTHS.map((month, index) => {
148
+ const isCurrentMonth = index === currentMonth.getMonth();
149
+ return (
150
+ <Button
151
+ key={month}
152
+ type={isCurrentMonth ? 'contained' : 'text'}
153
+ size="sm"
154
+ onPress={() => handleMonthSelect(index)}
155
+ disabled={disabled}
156
+ style={{ minWidth: 48, margin: 2 }}
157
+ >
158
+ <Text typography="caption" color={isCurrentMonth ? 'inverse' : 'primary'}>
159
+ {month}
160
+ </Text>
161
+ </Button>
162
+ );
163
+ })}
164
+ </div>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ // Render year selector
170
+ if (viewMode === 'years') {
171
+ return (
172
+ <div {...calendarProps}>
173
+ <div {...headerProps}>
174
+ <Button type="text" size="sm" onPress={goToPrevYearRange} disabled={disabled}>
175
+ <Icon name="chevron-left" size={16} />
176
+ </Button>
177
+ <Text typography="body2" weight="semibold">
178
+ {yearRange[0]} - {yearRange[yearRange.length - 1]}
179
+ </Text>
180
+ <Button type="text" size="sm" onPress={goToNextYearRange} disabled={disabled}>
181
+ <Icon name="chevron-right" size={16} />
182
+ </Button>
183
+ </div>
184
+ <div {...yearGridProps}>
185
+ {yearRange.map((yr) => {
186
+ const isCurrentYear = yr === currentMonth.getFullYear();
187
+ return (
188
+ <Button
189
+ key={yr}
190
+ type={isCurrentYear ? 'contained' : 'text'}
191
+ size="sm"
192
+ onPress={() => handleYearSelect(yr)}
193
+ disabled={disabled}
194
+ style={{ minWidth: 48, margin: 2 }}
195
+ >
196
+ <Text typography="caption" color={isCurrentYear ? 'inverse' : 'primary'}>
197
+ {yr}
198
+ </Text>
199
+ </Button>
200
+ );
201
+ })}
202
+ </div>
203
+ </div>
204
+ );
205
+ }
206
+
207
+ // Render calendar (default)
208
+ return (
209
+ <div {...calendarProps}>
210
+ {/* Header */}
211
+ <div {...headerProps}>
212
+ <Button type="text" size="sm" onPress={goToPrevMonth} disabled={disabled}>
213
+ <Icon name="chevron-left" size={16} />
214
+ </Button>
215
+ <Button type="text" size="sm" onPress={() => setViewMode('months')} disabled={disabled}>
216
+ <Text typography="body2" weight="semibold">
217
+ {monthShort} {year}
218
+ </Text>
219
+ </Button>
220
+ <Button type="text" size="sm" onPress={goToNextMonth} disabled={disabled}>
221
+ <Icon name="chevron-right" size={16} />
222
+ </Button>
223
+ </div>
224
+
225
+ {/* Weekday headers */}
226
+ <div {...weekdayRowProps}>
227
+ {WEEKDAYS.map((day, i) => (
228
+ <div key={i} {...weekdayCellProps}>
229
+ <Text typography="caption" color="secondary">
230
+ {day}
231
+ </Text>
232
+ </div>
233
+ ))}
234
+ </div>
235
+
236
+ {/* Calendar grid */}
237
+ <div {...gridProps}>
238
+ {days.map(({ date, isCurrentMonth }, index) => {
239
+ const selected = isSelected(date);
240
+ const today = isToday(date);
241
+ const dayDisabled = isDisabled(date);
242
+
243
+ const dayCellProps = getWebProps([
244
+ datePickerStyles.dayCell,
245
+ selected && datePickerStyles.selectedDay,
246
+ today && !selected && datePickerStyles.todayDay,
247
+ ].filter(Boolean));
248
+
249
+ const cellStyle: React.CSSProperties = {
250
+ opacity: (!isCurrentMonth || dayDisabled) ? 0.3 : 1,
251
+ };
252
+
253
+ return (
254
+ <div
255
+ key={index}
256
+ {...dayCellProps}
257
+ style={cellStyle}
258
+ >
259
+ <Button
260
+ type="text"
261
+ size="sm"
262
+ onPress={() => handleDayPress(date)}
263
+ disabled={dayDisabled}
264
+ style={{ minWidth: 24, minHeight: 24, padding: 0 }}
265
+ >
266
+ <Text
267
+ typography="caption"
268
+ color={selected ? 'inverse' : 'primary'}
269
+ >
270
+ {date.getDate()}
271
+ </Text>
272
+ </Button>
273
+ </div>
274
+ );
275
+ })}
276
+ </div>
277
+ </div>
278
+ );
279
+ };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { View, Text } from '@idealyst/components';
2
+ import { View } from 'react-native';
3
+ import { Text } from '@idealyst/components';
3
4
  import { DateInput } from './DateInput';
4
5
  import { TimeInput } from './TimeInput';
5
6
  import { datePickerStyles } from './styles';
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { Text } from '@idealyst/components';
4
+ import { DateInput } from './DateInput';
5
+ import { TimeInput } from './TimeInput';
6
+ import { datePickerStyles } from './styles';
7
+ import type { DateTimePickerProps } from './types';
8
+
9
+ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
10
+ value,
11
+ onChange,
12
+ label,
13
+ minDate,
14
+ maxDate,
15
+ timeMode = '12h',
16
+ minuteStep = 1,
17
+ disabled = false,
18
+ error,
19
+ style,
20
+ }) => {
21
+ const handleDateChange = (date: Date | null) => {
22
+ if (!date) {
23
+ onChange(null);
24
+ return;
25
+ }
26
+ // Preserve time from current value, or use noon as default
27
+ const hours = value?.getHours() ?? 12;
28
+ const minutes = value?.getMinutes() ?? 0;
29
+ const updated = new Date(date);
30
+ updated.setHours(hours, minutes, 0, 0);
31
+ onChange(updated);
32
+ };
33
+
34
+ const handleTimeChange = (time: Date | null) => {
35
+ if (!time) {
36
+ // Only clear time component, keep date if it exists
37
+ if (value) {
38
+ const updated = new Date(value);
39
+ updated.setHours(12, 0, 0, 0);
40
+ onChange(updated);
41
+ }
42
+ return;
43
+ }
44
+ // Preserve date from current value, or use today
45
+ const baseDate = value || new Date();
46
+ const updated = new Date(
47
+ baseDate.getFullYear(),
48
+ baseDate.getMonth(),
49
+ baseDate.getDate(),
50
+ time.getHours(),
51
+ time.getMinutes(),
52
+ 0,
53
+ 0
54
+ );
55
+ onChange(updated);
56
+ };
57
+
58
+ // Get web props for styled elements
59
+ const inputRowProps = getWebProps([datePickerStyles.inputRow]);
60
+
61
+ return (
62
+ <div style={style as React.CSSProperties}>
63
+ {label && (
64
+ <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
65
+ {label}
66
+ </Text>
67
+ )}
68
+ <div {...inputRowProps}>
69
+ <div style={{ flex: 1 }}>
70
+ <DateInput
71
+ value={value ?? undefined}
72
+ onChange={handleDateChange}
73
+ placeholder="MM/DD/YYYY"
74
+ minDate={minDate}
75
+ maxDate={maxDate}
76
+ disabled={disabled}
77
+ error={error}
78
+ />
79
+ </div>
80
+ <div style={{ flex: 1 }}>
81
+ <TimeInput
82
+ value={value ?? undefined}
83
+ onChange={handleTimeChange}
84
+ placeholder={timeMode === '24h' ? '14:30' : '2:30 PM'}
85
+ mode={timeMode}
86
+ minuteStep={minuteStep}
87
+ disabled={disabled}
88
+ />
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+ };
@@ -89,38 +89,27 @@ export const TimeInput: React.FC<TimeInputProps> = ({
89
89
  onChange(date);
90
90
  };
91
91
 
92
+ // Apply variants for input container
93
+ datePickerStyles.useVariants({
94
+ disabled,
95
+ error: !!error,
96
+ });
97
+
92
98
  return (
93
99
  <View style={style}>
94
100
  {label && (
95
- <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
101
+ <Text typography="body2" weight="medium" style={datePickerStyles.labelText}>
96
102
  {label}
97
103
  </Text>
98
104
  )}
99
- <View
100
- style={{
101
- flexDirection: 'row',
102
- alignItems: 'center',
103
- borderWidth: 1,
104
- borderColor: error ? '#ef4444' : '#d1d5db',
105
- borderRadius: 6,
106
- backgroundColor: disabled ? '#f3f4f6' : 'white',
107
- overflow: 'hidden',
108
- }}
109
- >
105
+ <View style={datePickerStyles.inputContainer}>
110
106
  <RNTextInput
111
107
  value={inputValue}
112
108
  onChangeText={handleInputChange}
113
109
  onBlur={handleInputBlur}
114
110
  placeholder={placeholder}
115
111
  editable={!disabled}
116
- style={{
117
- flex: 1,
118
- padding: 8,
119
- paddingHorizontal: 12,
120
- fontSize: 14,
121
- backgroundColor: 'transparent',
122
- color: disabled ? '#9ca3af' : '#111827',
123
- }}
112
+ style={datePickerStyles.textInput}
124
113
  />
125
114
  <Button
126
115
  type="text"
@@ -133,7 +122,7 @@ export const TimeInput: React.FC<TimeInputProps> = ({
133
122
  </Button>
134
123
  </View>
135
124
  {error && (
136
- <Text typography="caption" style={{ marginTop: 4, color: '#ef4444' }}>
125
+ <Text typography="caption" style={datePickerStyles.errorText}>
137
126
  {error}
138
127
  </Text>
139
128
  )}
@@ -144,14 +133,7 @@ export const TimeInput: React.FC<TimeInputProps> = ({
144
133
  animationType="fade"
145
134
  onRequestClose={() => setOpen(false)}
146
135
  >
147
- <View
148
- style={{
149
- flex: 1,
150
- justifyContent: 'center',
151
- alignItems: 'center',
152
- backgroundColor: 'rgba(0,0,0,0.5)',
153
- }}
154
- >
136
+ <View style={datePickerStyles.modalBackdrop}>
155
137
  <View style={datePickerStyles.popoverContent}>
156
138
  <TimePicker
157
139
  value={value ?? undefined}
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useRef, useEffect } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
2
3
  import { View, Text, Button, Icon } from '@idealyst/components';
3
4
  import { PositionedPortal } from '@idealyst/components/internal';
4
5
  import { TimePicker } from './TimePicker';
@@ -96,24 +97,24 @@ export const TimeInput: React.FC<TimeInputProps> = ({
96
97
  onChange(date);
97
98
  };
98
99
 
100
+ // Apply variants for input container
101
+ datePickerStyles.useVariants({
102
+ disabled,
103
+ error: !!error,
104
+ });
105
+
106
+ // Get web props for styled elements
107
+ const containerProps = getWebProps([datePickerStyles.inputContainer]);
108
+ const inputProps = getWebProps([datePickerStyles.textInput]);
109
+
99
110
  return (
100
111
  <View style={style}>
101
112
  {label && (
102
- <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
113
+ <Text typography="body2" weight="medium" style={datePickerStyles.labelText}>
103
114
  {label}
104
115
  </Text>
105
116
  )}
106
- <div
107
- ref={triggerRef}
108
- style={{
109
- display: 'flex',
110
- alignItems: 'center',
111
- border: `1px solid ${error ? '#ef4444' : '#d1d5db'}`,
112
- borderRadius: 6,
113
- backgroundColor: disabled ? '#f3f4f6' : 'white',
114
- overflow: 'hidden',
115
- }}
116
- >
117
+ <div ref={triggerRef} {...containerProps}>
117
118
  <input
118
119
  type="text"
119
120
  value={inputValue}
@@ -121,15 +122,7 @@ export const TimeInput: React.FC<TimeInputProps> = ({
121
122
  onBlur={handleInputBlur}
122
123
  placeholder={placeholder}
123
124
  disabled={disabled}
124
- style={{
125
- flex: 1,
126
- padding: '8px 12px',
127
- fontSize: 14,
128
- border: 'none',
129
- outline: 'none',
130
- backgroundColor: 'transparent',
131
- color: disabled ? '#9ca3af' : '#111827',
132
- }}
125
+ {...inputProps}
133
126
  />
134
127
  <Button
135
128
  type="text"
@@ -142,7 +135,7 @@ export const TimeInput: React.FC<TimeInputProps> = ({
142
135
  </Button>
143
136
  </div>
144
137
  {error && (
145
- <Text typography="caption" style={{ marginTop: 4, color: '#ef4444' }}>
138
+ <Text typography="caption" style={datePickerStyles.errorText}>
146
139
  {error}
147
140
  </Text>
148
141
  )}
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { View, Text, Button, Icon } from '@idealyst/components';
2
+ import { View } from 'react-native';
3
+ import { Text, Button, Icon } from '@idealyst/components';
3
4
  import { datePickerStyles } from './styles';
4
5
  import type { TimePickerProps } from './types';
5
6
 
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { Text, Button, Icon } from '@idealyst/components';
4
+ import { datePickerStyles } from './styles';
5
+ import type { TimePickerProps } from './types';
6
+
7
+ export const TimePicker: React.FC<TimePickerProps> = ({
8
+ value,
9
+ onChange,
10
+ mode = '12h',
11
+ minuteStep = 1,
12
+ disabled = false,
13
+ style,
14
+ }) => {
15
+ datePickerStyles.useVariants({ disabled });
16
+
17
+ const currentDate = value || new Date();
18
+ const hours = currentDate.getHours();
19
+ const minutes = currentDate.getMinutes();
20
+ const is12Hour = mode === '12h';
21
+ const isPM = hours >= 12;
22
+ const displayHours = is12Hour ? (hours % 12 || 12) : hours;
23
+
24
+ const updateTime = (newHours: number, newMinutes: number) => {
25
+ const updated = new Date(currentDate);
26
+ updated.setHours(newHours, newMinutes, 0, 0);
27
+ onChange(updated);
28
+ };
29
+
30
+ const incrementHours = () => {
31
+ const newHours = (hours + 1) % 24;
32
+ updateTime(newHours, minutes);
33
+ };
34
+
35
+ const decrementHours = () => {
36
+ const newHours = (hours - 1 + 24) % 24;
37
+ updateTime(newHours, minutes);
38
+ };
39
+
40
+ const incrementMinutes = () => {
41
+ const newMinutes = (minutes + minuteStep) % 60;
42
+ const hourChange = minutes + minuteStep >= 60 ? 1 : 0;
43
+ updateTime((hours + hourChange) % 24, newMinutes);
44
+ };
45
+
46
+ const decrementMinutes = () => {
47
+ const newMinutes = (minutes - minuteStep + 60) % 60;
48
+ const hourChange = minutes - minuteStep < 0 ? -1 : 0;
49
+ updateTime((hours + hourChange + 24) % 24, newMinutes);
50
+ };
51
+
52
+ const togglePeriod = () => {
53
+ const newHours = isPM ? hours - 12 : hours + 12;
54
+ updateTime(newHours, minutes);
55
+ };
56
+
57
+ // Get web props for styled elements
58
+ const timePickerProps = getWebProps([datePickerStyles.timePicker, style as any]);
59
+ const timeColumnsProps = getWebProps([datePickerStyles.timeColumns]);
60
+ const timeColumnProps = getWebProps([datePickerStyles.timeColumn]);
61
+ const timeSeparatorProps = getWebProps([datePickerStyles.timeSeparator]);
62
+
63
+ return (
64
+ <div {...timePickerProps}>
65
+ <div {...timeColumnsProps}>
66
+ {/* Hours column */}
67
+ <div {...timeColumnProps}>
68
+ <Button type="text" size="sm" onPress={incrementHours} disabled={disabled}>
69
+ <Icon name="chevron-up" size={20} />
70
+ </Button>
71
+ <Text typography="h3" weight="semibold">
72
+ {String(displayHours).padStart(2, '0')}
73
+ </Text>
74
+ <Button type="text" size="sm" onPress={decrementHours} disabled={disabled}>
75
+ <Icon name="chevron-down" size={20} />
76
+ </Button>
77
+ </div>
78
+
79
+ {/* Separator */}
80
+ <div {...timeSeparatorProps}>
81
+ <Text typography="h3" weight="semibold">:</Text>
82
+ </div>
83
+
84
+ {/* Minutes column */}
85
+ <div {...timeColumnProps}>
86
+ <Button type="text" size="sm" onPress={incrementMinutes} disabled={disabled}>
87
+ <Icon name="chevron-up" size={20} />
88
+ </Button>
89
+ <Text typography="h3" weight="semibold">
90
+ {String(minutes).padStart(2, '0')}
91
+ </Text>
92
+ <Button type="text" size="sm" onPress={decrementMinutes} disabled={disabled}>
93
+ <Icon name="chevron-down" size={20} />
94
+ </Button>
95
+ </div>
96
+
97
+ {/* AM/PM toggle for 12-hour mode */}
98
+ {is12Hour && (
99
+ <div {...timeColumnProps}>
100
+ <Button
101
+ type="outlined"
102
+ size="sm"
103
+ onPress={togglePeriod}
104
+ disabled={disabled}
105
+ >
106
+ {isPM ? 'PM' : 'AM'}
107
+ </Button>
108
+ </div>
109
+ )}
110
+ </div>
111
+ </div>
112
+ );
113
+ };
@@ -1,9 +1,9 @@
1
1
  // Components - native platform
2
- export { DatePicker } from './DatePicker';
3
- export { TimePicker } from './TimePicker';
2
+ export { DatePicker } from './DatePicker.native';
3
+ export { TimePicker } from './TimePicker.native';
4
4
  export { DateInput } from './DateInput.native';
5
5
  export { TimeInput } from './TimeInput.native';
6
- export { DateTimePicker } from './DateTimePicker';
6
+ export { DateTimePicker } from './DateTimePicker.native';
7
7
 
8
8
  // Types
9
9
  export type {
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- // Components
2
- export { DatePicker } from './DatePicker';
3
- export { TimePicker } from './TimePicker';
4
- export { DateInput } from './DateInput';
5
- export { TimeInput } from './TimeInput';
6
- export { DateTimePicker } from './DateTimePicker';
1
+ // Components - web platform
2
+ export { DatePicker } from './DatePicker.web';
3
+ export { TimePicker } from './TimePicker.web';
4
+ export { DateInput } from './DateInput.web';
5
+ export { TimeInput } from './TimeInput.web';
6
+ export { DateTimePicker } from './DateTimePicker.web';
7
7
 
8
8
  // Types
9
9
  export type {
package/src/styles.ts CHANGED
@@ -5,6 +5,11 @@ export type DatePickerVariants = {
5
5
  disabled: boolean;
6
6
  };
7
7
 
8
+ export type InputContainerVariants = {
9
+ disabled: boolean;
10
+ error: boolean;
11
+ };
12
+
8
13
  export const datePickerStyles = StyleSheet.create((theme: Theme) => {
9
14
  return {
10
15
  // Calendar container - compact
@@ -28,18 +33,27 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
28
33
  justifyContent: 'space-between',
29
34
  marginBottom: 4,
30
35
  paddingHorizontal: 2,
36
+ _web: {
37
+ display: 'flex',
38
+ },
31
39
  },
32
40
 
33
41
  calendarTitle: {
34
42
  flexDirection: 'row',
35
43
  alignItems: 'center',
36
44
  gap: 2,
45
+ _web: {
46
+ display: 'flex',
47
+ },
37
48
  },
38
49
 
39
50
  // Weekday header row
40
51
  weekdayRow: {
41
52
  flexDirection: 'row',
42
53
  marginBottom: 2,
54
+ _web: {
55
+ display: 'flex',
56
+ },
43
57
  },
44
58
 
45
59
  weekdayCell: {
@@ -47,12 +61,18 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
47
61
  height: 20,
48
62
  alignItems: 'center',
49
63
  justifyContent: 'center',
64
+ _web: {
65
+ display: 'flex',
66
+ },
50
67
  },
51
68
 
52
69
  // Calendar grid
53
70
  calendarGrid: {
54
71
  flexDirection: 'row',
55
72
  flexWrap: 'wrap',
73
+ _web: {
74
+ display: 'flex',
75
+ },
56
76
  },
57
77
 
58
78
  // Month selector grid (3x4)
@@ -61,6 +81,9 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
61
81
  flexWrap: 'wrap',
62
82
  justifyContent: 'center',
63
83
  paddingVertical: 8,
84
+ _web: {
85
+ display: 'flex',
86
+ },
64
87
  },
65
88
 
66
89
  // Year selector grid
@@ -69,6 +92,9 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
69
92
  flexWrap: 'wrap',
70
93
  justifyContent: 'center',
71
94
  paddingVertical: 8,
95
+ _web: {
96
+ display: 'flex',
97
+ },
72
98
  },
73
99
 
74
100
  // Individual day cell - compact
@@ -77,6 +103,9 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
77
103
  height: 28,
78
104
  alignItems: 'center',
79
105
  justifyContent: 'center',
106
+ _web: {
107
+ display: 'flex',
108
+ },
80
109
  },
81
110
 
82
111
  // Time picker container
@@ -97,12 +126,18 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
97
126
  flexDirection: 'row',
98
127
  gap: 8,
99
128
  alignItems: 'center',
129
+ _web: {
130
+ display: 'flex',
131
+ },
100
132
  },
101
133
 
102
134
  // Individual time column (hours, minutes, period)
103
135
  timeColumn: {
104
136
  alignItems: 'center',
105
137
  gap: 2,
138
+ _web: {
139
+ display: 'flex',
140
+ },
106
141
  },
107
142
 
108
143
  // Time separator (colon)
@@ -114,6 +149,9 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
114
149
  inputRow: {
115
150
  flexDirection: 'row',
116
151
  gap: 8,
152
+ _web: {
153
+ display: 'flex',
154
+ },
117
155
  },
118
156
 
119
157
  // Popover content wrapper
@@ -123,5 +161,94 @@ export const datePickerStyles = StyleSheet.create((theme: Theme) => {
123
161
  ...theme.shadows.lg,
124
162
  overflow: 'hidden',
125
163
  },
164
+
165
+ // Input container for DateInput/TimeInput
166
+ inputContainer: {
167
+ flexDirection: 'row',
168
+ alignItems: 'center',
169
+ borderWidth: 1,
170
+ borderRadius: 6,
171
+ overflow: 'hidden',
172
+ borderColor: theme.colors.border.primary,
173
+ backgroundColor: theme.colors.surface.primary,
174
+ variants: {
175
+ disabled: {
176
+ true: {
177
+ backgroundColor: theme.colors.surface.secondary,
178
+ },
179
+ false: {},
180
+ },
181
+ error: {
182
+ true: {
183
+ borderColor: theme.intents.error.primary,
184
+ },
185
+ false: {},
186
+ },
187
+ },
188
+ _web: {
189
+ display: 'flex',
190
+ flexDirection: 'row',
191
+ alignItems: 'center',
192
+ border: `1px solid ${theme.colors.border.primary}`,
193
+ },
194
+ },
195
+
196
+ // Text input inside the input container
197
+ textInput: {
198
+ flex: 1,
199
+ padding: 8,
200
+ paddingHorizontal: 12,
201
+ fontSize: 14,
202
+ backgroundColor: 'transparent',
203
+ color: theme.colors.text.primary,
204
+ variants: {
205
+ disabled: {
206
+ true: {
207
+ color: theme.colors.text.tertiary,
208
+ },
209
+ false: {},
210
+ },
211
+ },
212
+ _web: {
213
+ outline: 'none',
214
+ border: 'none',
215
+ },
216
+ },
217
+
218
+ // Error text below input
219
+ errorText: {
220
+ marginTop: 4,
221
+ color: theme.intents.error.primary,
222
+ },
223
+
224
+ // Label text above input
225
+ labelText: {
226
+ marginBottom: 4,
227
+ },
228
+
229
+ // Modal backdrop
230
+ modalBackdrop: {
231
+ flex: 1,
232
+ justifyContent: 'center',
233
+ alignItems: 'center',
234
+ backgroundColor: 'rgba(0,0,0,0.5)',
235
+ },
236
+
237
+ // Selected day styling
238
+ selectedDay: {
239
+ backgroundColor: theme.intents.primary.primary,
240
+ borderRadius: 14,
241
+ },
242
+
243
+ selectedDayText: {
244
+ color: theme.intents.primary.contrast,
245
+ },
246
+
247
+ // Today styling
248
+ todayDay: {
249
+ borderWidth: 1,
250
+ borderColor: theme.intents.primary.primary,
251
+ borderRadius: 14,
252
+ },
126
253
  } as const;
127
254
  });