@idealyst/datepicker 1.2.118 → 11.2.120
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 +3 -3
- package/src/DateInput.native.tsx +26 -11
- package/src/DateInput.web.tsx +33 -12
- package/src/DatePicker.native.tsx +38 -11
- package/src/DatePicker.styles.ts +49 -26
- package/src/DatePicker.web.tsx +47 -20
- package/src/DateTimePicker.native.tsx +3 -0
- package/src/DateTimePicker.web.tsx +3 -0
- package/src/InputStyles.ts +29 -0
- package/src/TimeInput.native.tsx +26 -11
- package/src/TimeInput.web.tsx +33 -12
- package/src/index.native.ts +1 -0
- package/src/index.ts +1 -0
- package/src/types.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/datepicker",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.2.120",
|
|
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,7 +36,7 @@
|
|
|
36
36
|
"publish:npm": "npm publish"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@idealyst/theme": "^
|
|
39
|
+
"@idealyst/theme": "^11.2.120",
|
|
40
40
|
"@mdi/js": ">=7.0.0",
|
|
41
41
|
"@mdi/react": ">=1.6.0",
|
|
42
42
|
"react": ">=16.8.0",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@idealyst/theme": "^
|
|
72
|
+
"@idealyst/theme": "^11.2.120",
|
|
73
73
|
"@mdi/js": "^7.4.47",
|
|
74
74
|
"@mdi/react": "^1.6.1",
|
|
75
75
|
"@types/react": "^19.1.0",
|
package/src/DateInput.native.tsx
CHANGED
|
@@ -13,6 +13,7 @@ export const DateInput: React.FC<DateInputProps> = ({
|
|
|
13
13
|
minDate,
|
|
14
14
|
maxDate,
|
|
15
15
|
disabled = false,
|
|
16
|
+
pressable = false,
|
|
16
17
|
error,
|
|
17
18
|
size = 'md',
|
|
18
19
|
style,
|
|
@@ -98,23 +99,37 @@ export const DateInput: React.FC<DateInputProps> = ({
|
|
|
98
99
|
{label}
|
|
99
100
|
</Text>
|
|
100
101
|
)}
|
|
101
|
-
|
|
102
|
-
<TextInput
|
|
103
|
-
value={inputValue}
|
|
104
|
-
onChangeText={handleInputChange}
|
|
105
|
-
onBlur={handleInputBlur}
|
|
106
|
-
placeholder={placeholder}
|
|
107
|
-
editable={!disabled}
|
|
108
|
-
style={textInputStyle}
|
|
109
|
-
/>
|
|
102
|
+
{pressable ? (
|
|
110
103
|
<TouchableOpacity
|
|
111
|
-
style={
|
|
104
|
+
style={inputContainerStyle}
|
|
112
105
|
onPress={() => !disabled && setOpen(true)}
|
|
113
106
|
disabled={disabled}
|
|
107
|
+
activeOpacity={0.7}
|
|
114
108
|
>
|
|
109
|
+
<Text style={[textInputStyle, !value && { color: (styles.pressableText as any)({ disabled, size }).placeholderColor }]}>
|
|
110
|
+
{value ? formatDate(value) : placeholder}
|
|
111
|
+
</Text>
|
|
115
112
|
<MaterialDesignIcons name="calendar" size={iconStyle.width} style={iconStyle} />
|
|
116
113
|
</TouchableOpacity>
|
|
117
|
-
|
|
114
|
+
) : (
|
|
115
|
+
<View style={inputContainerStyle}>
|
|
116
|
+
<TextInput
|
|
117
|
+
value={inputValue}
|
|
118
|
+
onChangeText={handleInputChange}
|
|
119
|
+
onBlur={handleInputBlur}
|
|
120
|
+
placeholder={placeholder}
|
|
121
|
+
editable={!disabled}
|
|
122
|
+
style={textInputStyle}
|
|
123
|
+
/>
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
style={iconButtonStyle}
|
|
126
|
+
onPress={() => !disabled && setOpen(true)}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
>
|
|
129
|
+
<MaterialDesignIcons name="calendar" size={iconStyle.width} style={iconStyle} />
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
</View>
|
|
132
|
+
)}
|
|
118
133
|
{error && (
|
|
119
134
|
<Text style={errorTextStyle}>
|
|
120
135
|
{error}
|
package/src/DateInput.web.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export const DateInput: React.FC<DateInputProps> = ({
|
|
|
18
18
|
minDate,
|
|
19
19
|
maxDate,
|
|
20
20
|
disabled = false,
|
|
21
|
+
pressable = false,
|
|
21
22
|
error,
|
|
22
23
|
size = 'md',
|
|
23
24
|
style,
|
|
@@ -108,30 +109,50 @@ export const DateInput: React.FC<DateInputProps> = ({
|
|
|
108
109
|
const errorProps = getWebProps([errorTextStyle]);
|
|
109
110
|
const popoverProps = getWebProps([popoverContentStyle]);
|
|
110
111
|
|
|
112
|
+
// Pressable text style (reuses input style but renders as a button)
|
|
113
|
+
const pressableTextStyle = (styles.pressableText as any)({ disabled, size });
|
|
114
|
+
const pressableTextProps = getWebProps([pressableTextStyle]);
|
|
115
|
+
|
|
111
116
|
return (
|
|
112
117
|
<div style={flattenStyle(style)}>
|
|
113
118
|
{label && (
|
|
114
119
|
<span {...labelProps}>{label}</span>
|
|
115
120
|
)}
|
|
116
|
-
|
|
117
|
-
<input
|
|
118
|
-
type="text"
|
|
119
|
-
value={inputValue}
|
|
120
|
-
onChange={handleInputChange}
|
|
121
|
-
onBlur={handleInputBlur}
|
|
122
|
-
placeholder={placeholder}
|
|
123
|
-
disabled={disabled}
|
|
124
|
-
{...inputProps}
|
|
125
|
-
/>
|
|
121
|
+
{pressable ? (
|
|
126
122
|
<button
|
|
127
123
|
type="button"
|
|
128
|
-
{...
|
|
124
|
+
{...containerProps}
|
|
125
|
+
ref={triggerRef as any}
|
|
126
|
+
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
|
|
129
127
|
onClick={() => !disabled && setOpen(!open)}
|
|
130
128
|
disabled={disabled}
|
|
131
129
|
>
|
|
130
|
+
<span {...pressableTextProps} style={!value ? { color: pressableTextStyle.placeholderColor } : undefined}>
|
|
131
|
+
{value ? formatDate(value) : placeholder}
|
|
132
|
+
</span>
|
|
132
133
|
<IconSvg path={mdiCalendar} size={iconSize} color={iconColor} />
|
|
133
134
|
</button>
|
|
134
|
-
|
|
135
|
+
) : (
|
|
136
|
+
<div {...containerProps} ref={triggerRef}>
|
|
137
|
+
<input
|
|
138
|
+
type="text"
|
|
139
|
+
value={inputValue}
|
|
140
|
+
onChange={handleInputChange}
|
|
141
|
+
onBlur={handleInputBlur}
|
|
142
|
+
placeholder={placeholder}
|
|
143
|
+
disabled={disabled}
|
|
144
|
+
{...inputProps}
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
{...iconButtonProps}
|
|
149
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
>
|
|
152
|
+
<IconSvg path={mdiCalendar} size={iconSize} color={iconColor} />
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
135
156
|
{error && (
|
|
136
157
|
<span {...errorProps}>{error}</span>
|
|
137
158
|
)}
|
|
@@ -2,19 +2,27 @@ import React, { useMemo, useState } from 'react';
|
|
|
2
2
|
import { View, Text, TouchableOpacity } from 'react-native';
|
|
3
3
|
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
|
|
4
4
|
import { datePickerCalendarStyles } from './DatePicker.styles';
|
|
5
|
-
import type { DatePickerProps } from './types';
|
|
5
|
+
import type { DatePickerProps, DayIndicator } from './types';
|
|
6
6
|
|
|
7
7
|
const WEEKDAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
8
8
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
9
9
|
|
|
10
10
|
type ViewMode = 'calendar' | 'months' | 'years';
|
|
11
11
|
|
|
12
|
+
function formatDateKey(date: Date): string {
|
|
13
|
+
const y = date.getFullYear();
|
|
14
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
15
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
16
|
+
return `${y}-${m}-${d}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export const DatePicker: React.FC<DatePickerProps> = ({
|
|
13
20
|
value,
|
|
14
21
|
onChange,
|
|
15
22
|
minDate,
|
|
16
23
|
maxDate,
|
|
17
24
|
disabled = false,
|
|
25
|
+
indicators,
|
|
18
26
|
style,
|
|
19
27
|
}) => {
|
|
20
28
|
const [currentMonth, setCurrentMonth] = useState(() => value || new Date());
|
|
@@ -44,6 +52,8 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
44
52
|
const selectorItemTextStyle = (styles.selectorItemText as any)({});
|
|
45
53
|
const selectorItemTextSelectedStyle = (styles.selectorItemTextSelected as any)({});
|
|
46
54
|
const iconStyle = (styles.iconColor as any)({});
|
|
55
|
+
const indicatorRowStyle = (styles.indicatorRow as any)({});
|
|
56
|
+
const indicatorStyle = (styles.indicator as any)({});
|
|
47
57
|
|
|
48
58
|
const { days, monthShort, year } = useMemo(() => {
|
|
49
59
|
const year = currentMonth.getFullYear();
|
|
@@ -71,13 +81,11 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
71
81
|
days.push({ date: new Date(year, month, day, 12, 0, 0, 0), isCurrentMonth: true });
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
// Next month padding (fill to
|
|
84
|
+
// Next month padding (always fill to 42 days = 6 rows for consistent height)
|
|
75
85
|
// Use noon (12:00) to avoid timezone issues when date crosses day boundaries
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
days.push({ date: new Date(year, month + 1, day, 12, 0, 0, 0), isCurrentMonth: false });
|
|
80
|
-
}
|
|
86
|
+
const totalCells = 42;
|
|
87
|
+
for (let day = 1; days.length < totalCells; day++) {
|
|
88
|
+
days.push({ date: new Date(year, month + 1, day, 12, 0, 0, 0), isCurrentMonth: false });
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
return { days, monthShort: MONTHS[month], year };
|
|
@@ -157,6 +165,13 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
157
165
|
setCurrentMonth(new Date(currentMonth.getFullYear() + 10, currentMonth.getMonth(), 1));
|
|
158
166
|
};
|
|
159
167
|
|
|
168
|
+
// Helper to get indicators for a date
|
|
169
|
+
const getIndicators = (date: Date): DayIndicator[] => {
|
|
170
|
+
if (!indicators) return [];
|
|
171
|
+
const key = formatDateKey(date);
|
|
172
|
+
return indicators[key] || [];
|
|
173
|
+
};
|
|
174
|
+
|
|
160
175
|
// Render month selector
|
|
161
176
|
if (viewMode === 'months') {
|
|
162
177
|
return (
|
|
@@ -176,7 +191,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
176
191
|
>
|
|
177
192
|
<Text style={titleTextStyle}>{year}</Text>
|
|
178
193
|
</TouchableOpacity>
|
|
179
|
-
<View style={{ width:
|
|
194
|
+
<View style={{ width: 32 }} />
|
|
180
195
|
</View>
|
|
181
196
|
<View style={monthGridStyle}>
|
|
182
197
|
{MONTHS.map((month, index) => {
|
|
@@ -305,20 +320,24 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
305
320
|
const today = isToday(date);
|
|
306
321
|
const dayDisabled = isDisabled(date);
|
|
307
322
|
const disabledDayButtonStyle = (styles.dayButton as any)({ disabled: dayDisabled });
|
|
323
|
+
const dayIndicators = getIndicators(date);
|
|
308
324
|
|
|
309
325
|
return (
|
|
310
326
|
<View
|
|
311
327
|
key={index}
|
|
312
328
|
style={[
|
|
313
329
|
dayCellStyle,
|
|
314
|
-
selected && selectedDayStyle,
|
|
315
330
|
!isCurrentMonth && { opacity: 0.3 },
|
|
316
|
-
today && !selected && todayDayStyle,
|
|
317
331
|
dayDisabled && { opacity: 0.3 },
|
|
318
332
|
]}
|
|
319
333
|
>
|
|
320
334
|
<TouchableOpacity
|
|
321
|
-
style={[
|
|
335
|
+
style={[
|
|
336
|
+
dayButtonStyle,
|
|
337
|
+
dayDisabled && disabledDayButtonStyle,
|
|
338
|
+
selected && selectedDayStyle,
|
|
339
|
+
today && !selected && todayDayStyle,
|
|
340
|
+
]}
|
|
322
341
|
onPress={() => handleDayPress(date)}
|
|
323
342
|
disabled={dayDisabled}
|
|
324
343
|
>
|
|
@@ -331,6 +350,14 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
331
350
|
{date.getDate()}
|
|
332
351
|
</Text>
|
|
333
352
|
</TouchableOpacity>
|
|
353
|
+
<View style={indicatorRowStyle}>
|
|
354
|
+
{dayIndicators.slice(0, 3).map((ind, i) => (
|
|
355
|
+
<View
|
|
356
|
+
key={ind.key ?? i}
|
|
357
|
+
style={[indicatorStyle, { backgroundColor: ind.color }]}
|
|
358
|
+
/>
|
|
359
|
+
))}
|
|
360
|
+
</View>
|
|
334
361
|
</View>
|
|
335
362
|
);
|
|
336
363
|
})}
|
package/src/DatePicker.styles.ts
CHANGED
|
@@ -19,12 +19,12 @@ export type DatePickerDynamicProps = {
|
|
|
19
19
|
* DatePicker calendar styles with theme reactivity.
|
|
20
20
|
*/
|
|
21
21
|
export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme: Theme) => ({
|
|
22
|
-
// Calendar container
|
|
22
|
+
// Calendar container
|
|
23
23
|
calendar: (_props: DatePickerDynamicProps) => ({
|
|
24
|
-
padding:
|
|
24
|
+
padding: 12,
|
|
25
25
|
backgroundColor: theme.colors.surface.primary,
|
|
26
26
|
borderRadius: 6,
|
|
27
|
-
width:
|
|
27
|
+
width: 280,
|
|
28
28
|
variants: {
|
|
29
29
|
disabled: {
|
|
30
30
|
true: { opacity: 0.6 },
|
|
@@ -38,7 +38,7 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
38
38
|
flexDirection: 'row' as const,
|
|
39
39
|
alignItems: 'center' as const,
|
|
40
40
|
justifyContent: 'space-between' as const,
|
|
41
|
-
marginBottom:
|
|
41
|
+
marginBottom: 8,
|
|
42
42
|
paddingHorizontal: 2,
|
|
43
43
|
_web: {
|
|
44
44
|
display: 'flex',
|
|
@@ -57,15 +57,15 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
57
57
|
// Weekday header row
|
|
58
58
|
weekdayRow: (_props: DatePickerDynamicProps) => ({
|
|
59
59
|
flexDirection: 'row' as const,
|
|
60
|
-
marginBottom:
|
|
60
|
+
marginBottom: 4,
|
|
61
61
|
_web: {
|
|
62
62
|
display: 'flex',
|
|
63
63
|
},
|
|
64
64
|
}),
|
|
65
65
|
|
|
66
66
|
weekdayCell: (_props: DatePickerDynamicProps) => ({
|
|
67
|
-
width:
|
|
68
|
-
height:
|
|
67
|
+
width: 36,
|
|
68
|
+
height: 24,
|
|
69
69
|
alignItems: 'center' as const,
|
|
70
70
|
justifyContent: 'center' as const,
|
|
71
71
|
_web: {
|
|
@@ -104,12 +104,13 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
104
104
|
},
|
|
105
105
|
}),
|
|
106
106
|
|
|
107
|
-
// Individual day cell -
|
|
107
|
+
// Individual day cell - contains button + indicator row
|
|
108
108
|
dayCell: (_props: DatePickerDynamicProps) => ({
|
|
109
|
-
width:
|
|
110
|
-
height:
|
|
109
|
+
width: 36,
|
|
110
|
+
height: 42,
|
|
111
111
|
alignItems: 'center' as const,
|
|
112
|
-
justifyContent: '
|
|
112
|
+
justifyContent: 'flex-start' as const,
|
|
113
|
+
paddingTop: 2,
|
|
113
114
|
_web: {
|
|
114
115
|
display: 'flex',
|
|
115
116
|
},
|
|
@@ -117,8 +118,8 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
117
118
|
|
|
118
119
|
// Navigation button
|
|
119
120
|
navButton: (_props: DatePickerDynamicProps) => ({
|
|
120
|
-
width:
|
|
121
|
-
height:
|
|
121
|
+
width: 32,
|
|
122
|
+
height: 32,
|
|
122
123
|
alignItems: 'center' as const,
|
|
123
124
|
justifyContent: 'center' as const,
|
|
124
125
|
borderRadius: 4,
|
|
@@ -158,18 +159,18 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
158
159
|
}),
|
|
159
160
|
|
|
160
161
|
titleText: (_props: DatePickerDynamicProps) => ({
|
|
161
|
-
fontSize:
|
|
162
|
+
fontSize: 15,
|
|
162
163
|
fontWeight: '600' as const,
|
|
163
164
|
color: theme.colors.text.primary,
|
|
164
165
|
}),
|
|
165
166
|
|
|
166
|
-
// Day button - fills
|
|
167
|
+
// Day button - fills cell for better click handling
|
|
167
168
|
dayButton: (_props: DatePickerDynamicProps) => ({
|
|
168
|
-
width:
|
|
169
|
-
height:
|
|
169
|
+
width: 32,
|
|
170
|
+
height: 32,
|
|
170
171
|
alignItems: 'center' as const,
|
|
171
172
|
justifyContent: 'center' as const,
|
|
172
|
-
borderRadius:
|
|
173
|
+
borderRadius: 16,
|
|
173
174
|
backgroundColor: 'transparent',
|
|
174
175
|
borderWidth: 0,
|
|
175
176
|
_web: {
|
|
@@ -188,7 +189,7 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
188
189
|
}),
|
|
189
190
|
|
|
190
191
|
dayText: (_props: DatePickerDynamicProps) => ({
|
|
191
|
-
fontSize:
|
|
192
|
+
fontSize: 14,
|
|
192
193
|
color: theme.colors.text.primary,
|
|
193
194
|
_web: {
|
|
194
195
|
pointerEvents: 'none',
|
|
@@ -196,7 +197,7 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
196
197
|
}),
|
|
197
198
|
|
|
198
199
|
weekdayText: (_props: DatePickerDynamicProps) => ({
|
|
199
|
-
fontSize:
|
|
200
|
+
fontSize: 12,
|
|
200
201
|
color: theme.colors.text.secondary,
|
|
201
202
|
}),
|
|
202
203
|
|
|
@@ -232,7 +233,7 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
232
233
|
}),
|
|
233
234
|
|
|
234
235
|
selectorItemText: (_props: DatePickerDynamicProps) => ({
|
|
235
|
-
fontSize:
|
|
236
|
+
fontSize: 13,
|
|
236
237
|
color: theme.colors.text.primary,
|
|
237
238
|
_web: {
|
|
238
239
|
pointerEvents: 'none',
|
|
@@ -249,7 +250,7 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
249
250
|
// Selected day styling
|
|
250
251
|
selectedDay: (_props: DatePickerDynamicProps) => ({
|
|
251
252
|
backgroundColor: theme.intents.primary.primary,
|
|
252
|
-
borderRadius:
|
|
253
|
+
borderRadius: 16,
|
|
253
254
|
_web: {
|
|
254
255
|
background: theme.intents.primary.primary,
|
|
255
256
|
},
|
|
@@ -262,15 +263,37 @@ export const datePickerCalendarStyles = defineStyle('DatePickerCalendar', (theme
|
|
|
262
263
|
},
|
|
263
264
|
}),
|
|
264
265
|
|
|
265
|
-
// Today styling -
|
|
266
|
+
// Today styling - outlined circle (no fill)
|
|
266
267
|
todayDay: (_props: DatePickerDynamicProps) => ({
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
borderWidth: 1,
|
|
269
|
+
borderColor: theme.intents.primary.primary,
|
|
270
|
+
borderRadius: 16,
|
|
271
|
+
backgroundColor: 'transparent',
|
|
269
272
|
_web: {
|
|
270
|
-
background:
|
|
273
|
+
background: 'transparent',
|
|
274
|
+
boxSizing: 'border-box',
|
|
271
275
|
},
|
|
272
276
|
}),
|
|
273
277
|
|
|
278
|
+
// Indicator row below the day number
|
|
279
|
+
indicatorRow: (_props: DatePickerDynamicProps) => ({
|
|
280
|
+
flexDirection: 'row' as const,
|
|
281
|
+
alignItems: 'center' as const,
|
|
282
|
+
justifyContent: 'center' as const,
|
|
283
|
+
height: 6,
|
|
284
|
+
gap: 2,
|
|
285
|
+
_web: {
|
|
286
|
+
display: 'flex',
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
|
|
290
|
+
// Individual indicator dot
|
|
291
|
+
indicator: (_props: DatePickerDynamicProps) => ({
|
|
292
|
+
width: 4,
|
|
293
|
+
height: 4,
|
|
294
|
+
borderRadius: 2,
|
|
295
|
+
}),
|
|
296
|
+
|
|
274
297
|
// Icon color helper
|
|
275
298
|
iconColor: (_props: DatePickerDynamicProps) => ({
|
|
276
299
|
color: theme.colors.text.primary,
|
package/src/DatePicker.web.tsx
CHANGED
|
@@ -3,19 +3,27 @@ import { getWebProps } from 'react-native-unistyles/web';
|
|
|
3
3
|
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
|
4
4
|
import { IconSvg } from './IconSvg.web';
|
|
5
5
|
import { datePickerCalendarStyles } from './DatePicker.styles';
|
|
6
|
-
import type { DatePickerProps } from './types';
|
|
6
|
+
import type { DatePickerProps, DayIndicator } from './types';
|
|
7
7
|
|
|
8
8
|
const WEEKDAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
9
9
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
10
10
|
|
|
11
11
|
type ViewMode = 'calendar' | 'months' | 'years';
|
|
12
12
|
|
|
13
|
+
function formatDateKey(date: Date): string {
|
|
14
|
+
const y = date.getFullYear();
|
|
15
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
16
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
17
|
+
return `${y}-${m}-${d}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export const DatePicker: React.FC<DatePickerProps> = ({
|
|
14
21
|
value,
|
|
15
22
|
onChange,
|
|
16
23
|
minDate,
|
|
17
24
|
maxDate,
|
|
18
25
|
disabled = false,
|
|
26
|
+
indicators,
|
|
19
27
|
style,
|
|
20
28
|
}) => {
|
|
21
29
|
const [currentMonth, setCurrentMonth] = useState(() => value || new Date());
|
|
@@ -54,13 +62,11 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
54
62
|
days.push({ date: new Date(year, month, day, 12, 0, 0, 0), isCurrentMonth: true });
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
// Next month padding (fill to
|
|
65
|
+
// Next month padding (always fill to 42 days = 6 rows for consistent height)
|
|
58
66
|
// Use noon (12:00) to avoid timezone issues when date crosses day boundaries
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
days.push({ date: new Date(year, month + 1, day, 12, 0, 0, 0), isCurrentMonth: false });
|
|
63
|
-
}
|
|
67
|
+
const totalCells = 42;
|
|
68
|
+
for (let day = 1; days.length < totalCells; day++) {
|
|
69
|
+
days.push({ date: new Date(year, month + 1, day, 12, 0, 0, 0), isCurrentMonth: false });
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
return { days, monthShort: MONTHS[month], year };
|
|
@@ -157,6 +163,8 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
157
163
|
const selectorItemTextStyle = (styles.selectorItemText as any)({});
|
|
158
164
|
const selectorItemTextSelectedStyle = (styles.selectorItemTextSelected as any)({});
|
|
159
165
|
const iconColorStyle = (styles.iconColor as any)({});
|
|
166
|
+
const indicatorRowStyle = (styles.indicatorRow as any)({});
|
|
167
|
+
const indicatorStyle = (styles.indicator as any)({});
|
|
160
168
|
|
|
161
169
|
// Get web props for all elements
|
|
162
170
|
const calendarProps = getWebProps([calendarStyle, style as any]);
|
|
@@ -174,6 +182,14 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
174
182
|
const selectorItemSelectedProps = getWebProps([selectorItemStyle, selectorItemSelectedStyle]);
|
|
175
183
|
const selectorItemTextProps = getWebProps([selectorItemTextStyle]);
|
|
176
184
|
const selectorItemTextSelectedProps = getWebProps([selectorItemTextStyle, selectorItemTextSelectedStyle]);
|
|
185
|
+
const indicatorRowProps = getWebProps([indicatorRowStyle]);
|
|
186
|
+
|
|
187
|
+
// Helper to get indicators for a date
|
|
188
|
+
const getIndicators = (date: Date): DayIndicator[] => {
|
|
189
|
+
if (!indicators) return [];
|
|
190
|
+
const key = formatDateKey(date);
|
|
191
|
+
return indicators[key] || [];
|
|
192
|
+
};
|
|
177
193
|
|
|
178
194
|
// Render month selector
|
|
179
195
|
if (viewMode === 'months') {
|
|
@@ -196,7 +212,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
196
212
|
>
|
|
197
213
|
<span {...titleTextProps}>{year}</span>
|
|
198
214
|
</button>
|
|
199
|
-
<div style={{ width:
|
|
215
|
+
<div style={{ width: 32 }} />
|
|
200
216
|
</div>
|
|
201
217
|
<div {...monthGridProps}>
|
|
202
218
|
{MONTHS.map((month, index) => {
|
|
@@ -332,6 +348,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
332
348
|
const selected = isSelected(date);
|
|
333
349
|
const today = isToday(date);
|
|
334
350
|
const dayDisabled = isDisabled(date);
|
|
351
|
+
const dayIndicators = getIndicators(date);
|
|
335
352
|
|
|
336
353
|
// Get appropriate button props (className and ref only)
|
|
337
354
|
const buttonProps = dayDisabled
|
|
@@ -343,18 +360,28 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
343
360
|
: dayButtonProps;
|
|
344
361
|
|
|
345
362
|
return (
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
363
|
+
<div key={index} {...dayCellProps}>
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
className={buttonProps.className}
|
|
367
|
+
style={{ opacity: (!isCurrentMonthDay || dayDisabled) ? 0.3 : 1 }}
|
|
368
|
+
onClick={() => handleDayPress(date)}
|
|
369
|
+
disabled={dayDisabled}
|
|
370
|
+
>
|
|
371
|
+
<span {...(selected ? selectedDayTextProps : dayTextProps)}>
|
|
372
|
+
{date.getDate()}
|
|
373
|
+
</span>
|
|
374
|
+
</button>
|
|
375
|
+
<div {...indicatorRowProps}>
|
|
376
|
+
{dayIndicators.slice(0, 3).map((ind, i) => (
|
|
377
|
+
<div
|
|
378
|
+
key={ind.key ?? i}
|
|
379
|
+
className={getWebProps([indicatorStyle]).className}
|
|
380
|
+
style={{ backgroundColor: ind.color }}
|
|
381
|
+
/>
|
|
382
|
+
))}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
358
385
|
);
|
|
359
386
|
})}
|
|
360
387
|
</div>
|
|
@@ -14,6 +14,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
14
14
|
timeMode = '12h',
|
|
15
15
|
minuteStep = 1,
|
|
16
16
|
disabled = false,
|
|
17
|
+
pressable = false,
|
|
17
18
|
error,
|
|
18
19
|
size = 'md',
|
|
19
20
|
style,
|
|
@@ -91,6 +92,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
91
92
|
minDate={minDate}
|
|
92
93
|
maxDate={maxDate}
|
|
93
94
|
disabled={disabled}
|
|
95
|
+
pressable={pressable}
|
|
94
96
|
error={error}
|
|
95
97
|
size={size}
|
|
96
98
|
/>
|
|
@@ -103,6 +105,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
103
105
|
mode={timeMode}
|
|
104
106
|
minuteStep={minuteStep}
|
|
105
107
|
disabled={disabled}
|
|
108
|
+
pressable={pressable}
|
|
106
109
|
size={size}
|
|
107
110
|
/>
|
|
108
111
|
</View>
|
|
@@ -14,6 +14,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
14
14
|
timeMode = '12h',
|
|
15
15
|
minuteStep = 1,
|
|
16
16
|
disabled = false,
|
|
17
|
+
pressable = false,
|
|
17
18
|
error,
|
|
18
19
|
size = 'md',
|
|
19
20
|
style,
|
|
@@ -95,6 +96,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
95
96
|
minDate={minDate}
|
|
96
97
|
maxDate={maxDate}
|
|
97
98
|
disabled={disabled}
|
|
99
|
+
pressable={pressable}
|
|
98
100
|
error={error}
|
|
99
101
|
size={size}
|
|
100
102
|
/>
|
|
@@ -107,6 +109,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
107
109
|
mode={timeMode}
|
|
108
110
|
minuteStep={minuteStep}
|
|
109
111
|
disabled={disabled}
|
|
112
|
+
pressable={pressable}
|
|
110
113
|
size={size}
|
|
111
114
|
/>
|
|
112
115
|
</div>
|
package/src/InputStyles.ts
CHANGED
|
@@ -195,6 +195,35 @@ export const dateTimeInputStyles = defineStyle('DateTimeInput', (theme: Theme) =
|
|
|
195
195
|
color: theme.intents.primary.primary,
|
|
196
196
|
}),
|
|
197
197
|
|
|
198
|
+
// Pressable text (used when pressable=true instead of a text input)
|
|
199
|
+
pressableText: (_props: InputDynamicProps) => ({
|
|
200
|
+
flex: 1,
|
|
201
|
+
minWidth: 0,
|
|
202
|
+
color: theme.colors.text.primary,
|
|
203
|
+
fontWeight: '400' as const,
|
|
204
|
+
textAlign: 'left' as const,
|
|
205
|
+
// Default font size for when size variant isn't specified
|
|
206
|
+
fontSize: theme.sizes.input.md.fontSize,
|
|
207
|
+
// Placeholder color for native when no value is set
|
|
208
|
+
placeholderColor: theme.colors.text.tertiary,
|
|
209
|
+
_web: {
|
|
210
|
+
overflow: 'hidden',
|
|
211
|
+
textOverflow: 'ellipsis',
|
|
212
|
+
whiteSpace: 'nowrap',
|
|
213
|
+
pointerEvents: 'none',
|
|
214
|
+
},
|
|
215
|
+
variants: {
|
|
216
|
+
disabled: {
|
|
217
|
+
true: { color: theme.colors.text.tertiary },
|
|
218
|
+
false: { color: theme.colors.text.primary },
|
|
219
|
+
},
|
|
220
|
+
// $iterator expands for each input size
|
|
221
|
+
size: {
|
|
222
|
+
fontSize: theme.sizes.$input.fontSize,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
|
|
198
227
|
// Icon color helper
|
|
199
228
|
iconColor: (_props: InputDynamicProps) => ({
|
|
200
229
|
color: theme.colors.text.secondary,
|
package/src/TimeInput.native.tsx
CHANGED
|
@@ -13,6 +13,7 @@ export const TimeInput: React.FC<TimeInputProps> = ({
|
|
|
13
13
|
mode = '12h',
|
|
14
14
|
minuteStep = 1,
|
|
15
15
|
disabled = false,
|
|
16
|
+
pressable = false,
|
|
16
17
|
error,
|
|
17
18
|
size = 'md',
|
|
18
19
|
style,
|
|
@@ -118,23 +119,37 @@ export const TimeInput: React.FC<TimeInputProps> = ({
|
|
|
118
119
|
{label}
|
|
119
120
|
</Text>
|
|
120
121
|
)}
|
|
121
|
-
|
|
122
|
-
<TextInput
|
|
123
|
-
value={inputValue}
|
|
124
|
-
onChangeText={handleInputChange}
|
|
125
|
-
onBlur={handleInputBlur}
|
|
126
|
-
placeholder={placeholder}
|
|
127
|
-
editable={!disabled}
|
|
128
|
-
style={textInputStyle}
|
|
129
|
-
/>
|
|
122
|
+
{pressable ? (
|
|
130
123
|
<TouchableOpacity
|
|
131
|
-
style={
|
|
124
|
+
style={inputContainerStyle}
|
|
132
125
|
onPress={() => !disabled && setOpen(true)}
|
|
133
126
|
disabled={disabled}
|
|
127
|
+
activeOpacity={0.7}
|
|
134
128
|
>
|
|
129
|
+
<Text style={[textInputStyle, !value && { color: (styles.pressableText as any)({ disabled, size }).placeholderColor }]}>
|
|
130
|
+
{value ? formatTime(value) : placeholder}
|
|
131
|
+
</Text>
|
|
135
132
|
<MaterialDesignIcons name="clock-outline" size={iconStyle.width} style={iconStyle} />
|
|
136
133
|
</TouchableOpacity>
|
|
137
|
-
|
|
134
|
+
) : (
|
|
135
|
+
<View style={inputContainerStyle}>
|
|
136
|
+
<TextInput
|
|
137
|
+
value={inputValue}
|
|
138
|
+
onChangeText={handleInputChange}
|
|
139
|
+
onBlur={handleInputBlur}
|
|
140
|
+
placeholder={placeholder}
|
|
141
|
+
editable={!disabled}
|
|
142
|
+
style={textInputStyle}
|
|
143
|
+
/>
|
|
144
|
+
<TouchableOpacity
|
|
145
|
+
style={iconButtonStyle}
|
|
146
|
+
onPress={() => !disabled && setOpen(true)}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
>
|
|
149
|
+
<MaterialDesignIcons name="clock-outline" size={iconStyle.width} style={iconStyle} />
|
|
150
|
+
</TouchableOpacity>
|
|
151
|
+
</View>
|
|
152
|
+
)}
|
|
138
153
|
{error && (
|
|
139
154
|
<Text style={errorTextStyle}>
|
|
140
155
|
{error}
|
package/src/TimeInput.web.tsx
CHANGED
|
@@ -17,6 +17,7 @@ export const TimeInput: React.FC<TimeInputProps> = ({
|
|
|
17
17
|
mode = '12h',
|
|
18
18
|
minuteStep = 1,
|
|
19
19
|
disabled = false,
|
|
20
|
+
pressable = false,
|
|
20
21
|
error,
|
|
21
22
|
size = 'md',
|
|
22
23
|
style,
|
|
@@ -132,30 +133,50 @@ export const TimeInput: React.FC<TimeInputProps> = ({
|
|
|
132
133
|
const errorProps = getWebProps([errorTextStyle]);
|
|
133
134
|
const popoverProps = getWebProps([popoverContentStyle]);
|
|
134
135
|
|
|
136
|
+
// Pressable text style (reuses input style but renders as a button)
|
|
137
|
+
const pressableTextStyle = (styles.pressableText as any)({ disabled, size });
|
|
138
|
+
const pressableTextProps = getWebProps([pressableTextStyle]);
|
|
139
|
+
|
|
135
140
|
return (
|
|
136
141
|
<div style={style as React.CSSProperties}>
|
|
137
142
|
{label && (
|
|
138
143
|
<span {...labelProps}>{label}</span>
|
|
139
144
|
)}
|
|
140
|
-
|
|
141
|
-
<input
|
|
142
|
-
type="text"
|
|
143
|
-
value={inputValue}
|
|
144
|
-
onChange={handleInputChange}
|
|
145
|
-
onBlur={handleInputBlur}
|
|
146
|
-
placeholder={placeholder}
|
|
147
|
-
disabled={disabled}
|
|
148
|
-
{...inputProps}
|
|
149
|
-
/>
|
|
145
|
+
{pressable ? (
|
|
150
146
|
<button
|
|
151
147
|
type="button"
|
|
152
|
-
{...
|
|
148
|
+
{...containerProps}
|
|
149
|
+
ref={triggerRef as any}
|
|
150
|
+
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
|
|
153
151
|
onClick={() => !disabled && setOpen(!open)}
|
|
154
152
|
disabled={disabled}
|
|
155
153
|
>
|
|
154
|
+
<span {...pressableTextProps} style={!value ? { color: pressableTextStyle.placeholderColor } : undefined}>
|
|
155
|
+
{value ? formatTime(value) : placeholder}
|
|
156
|
+
</span>
|
|
156
157
|
<IconSvg path={mdiClockOutline} size={iconSize} color={iconColor} />
|
|
157
158
|
</button>
|
|
158
|
-
|
|
159
|
+
) : (
|
|
160
|
+
<div {...containerProps} ref={triggerRef}>
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
value={inputValue}
|
|
164
|
+
onChange={handleInputChange}
|
|
165
|
+
onBlur={handleInputBlur}
|
|
166
|
+
placeholder={placeholder}
|
|
167
|
+
disabled={disabled}
|
|
168
|
+
{...inputProps}
|
|
169
|
+
/>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
{...iconButtonProps}
|
|
173
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
174
|
+
disabled={disabled}
|
|
175
|
+
>
|
|
176
|
+
<IconSvg path={mdiClockOutline} size={iconSize} color={iconColor} />
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
159
180
|
{error && (
|
|
160
181
|
<span {...errorProps}>{error}</span>
|
|
161
182
|
)}
|
package/src/index.native.ts
CHANGED
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { ViewStyle } from 'react-native';
|
|
2
2
|
import type { Size } from '@idealyst/theme';
|
|
3
3
|
|
|
4
|
+
export interface DayIndicator {
|
|
5
|
+
color: string;
|
|
6
|
+
key?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
export interface DatePickerProps {
|
|
5
10
|
value?: Date;
|
|
6
11
|
onChange: (date: Date) => void;
|
|
7
12
|
minDate?: Date;
|
|
8
13
|
maxDate?: Date;
|
|
9
14
|
disabled?: boolean;
|
|
15
|
+
/** Indicators to render below specific dates. Key format: "YYYY-MM-DD" */
|
|
16
|
+
indicators?: Record<string, DayIndicator[]>;
|
|
10
17
|
style?: ViewStyle;
|
|
11
18
|
}
|
|
12
19
|
|
|
@@ -27,6 +34,8 @@ export interface DateInputProps {
|
|
|
27
34
|
minDate?: Date;
|
|
28
35
|
maxDate?: Date;
|
|
29
36
|
disabled?: boolean;
|
|
37
|
+
/** When true, the entire input area is pressable to open the calendar instead of being a text input. */
|
|
38
|
+
pressable?: boolean;
|
|
30
39
|
error?: string;
|
|
31
40
|
size?: Size;
|
|
32
41
|
style?: ViewStyle;
|
|
@@ -40,6 +49,8 @@ export interface TimeInputProps {
|
|
|
40
49
|
mode?: '12h' | '24h';
|
|
41
50
|
minuteStep?: number;
|
|
42
51
|
disabled?: boolean;
|
|
52
|
+
/** When true, the entire input area is pressable to open the time picker instead of being a text input. */
|
|
53
|
+
pressable?: boolean;
|
|
43
54
|
error?: string;
|
|
44
55
|
size?: Size;
|
|
45
56
|
style?: ViewStyle;
|
|
@@ -54,6 +65,8 @@ export interface DateTimePickerProps {
|
|
|
54
65
|
timeMode?: '12h' | '24h';
|
|
55
66
|
minuteStep?: number;
|
|
56
67
|
disabled?: boolean;
|
|
68
|
+
/** When true, both input areas are pressable to open their pickers instead of being text inputs. */
|
|
69
|
+
pressable?: boolean;
|
|
57
70
|
error?: string;
|
|
58
71
|
size?: Size;
|
|
59
72
|
style?: ViewStyle;
|