@ecohouse/ui 0.1.4 → 0.1.5
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/README.md +33 -13
- package/dist/index.cjs +805 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -10
- package/dist/index.d.ts +113 -10
- package/dist/index.js +802 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.tsx +19 -71
- package/src/components/DatePicker/DatePicker.stories.tsx +172 -0
- package/src/components/DatePicker/DatePicker.tsx +628 -0
- package/src/components/DatePicker/index.ts +7 -0
- package/src/components/index.ts +8 -0
- package/src/icons/CalendarOutline/CalendarOutlineIcon.tsx +25 -0
- package/src/icons/CalendarOutline/CalendarOutlineIcon.web.tsx +31 -0
- package/src/icons/CalendarOutline/calendarOutlinePath.ts +3 -0
- package/src/icons/CalendarOutline/index.ts +1 -0
- package/src/icons/ChevronLeft/ChevronLeftIcon.tsx +21 -0
- package/src/icons/ChevronLeft/ChevronLeftIcon.web.tsx +27 -0
- package/src/icons/ChevronLeft/chevronLeftPath.ts +2 -0
- package/src/icons/ChevronLeft/index.ts +1 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/registry.ts +17 -2
- package/src/index.ts +20 -1
- package/src/primitives/MenuItem/MenuItem.stories.tsx +98 -0
- package/src/primitives/MenuItem/MenuItem.tsx +138 -0
- package/src/primitives/MenuItem/index.ts +2 -0
- package/src/primitives/index.ts +2 -0
- package/src/theme/colors.ts +1 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/typography.ts +40 -0
- package/src/utils/dateUtils.ts +86 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentRef,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
Platform,
|
|
12
|
+
Pressable,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
View,
|
|
16
|
+
type StyleProp,
|
|
17
|
+
type ViewProps,
|
|
18
|
+
type ViewStyle,
|
|
19
|
+
} from "react-native";
|
|
20
|
+
import { CalendarOutlineIcon, ChevronLeftIcon } from "../../icons";
|
|
21
|
+
import { colors, datePickerTypography } from "../../theme";
|
|
22
|
+
import {
|
|
23
|
+
addMonths,
|
|
24
|
+
formatDate,
|
|
25
|
+
getMonthMatrix,
|
|
26
|
+
isBefore,
|
|
27
|
+
isDateDisabled,
|
|
28
|
+
isSameDay,
|
|
29
|
+
isSameMonth,
|
|
30
|
+
isWithinRange,
|
|
31
|
+
startOfDay,
|
|
32
|
+
} from "../../utils/dateUtils";
|
|
33
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
34
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
35
|
+
|
|
36
|
+
/** Monday-first — override via `monthNames` for other locales. */
|
|
37
|
+
const DEFAULT_MONTH_NAMES = [
|
|
38
|
+
"Հունվար",
|
|
39
|
+
"Փետրվար",
|
|
40
|
+
"Մարտ",
|
|
41
|
+
"Ապրիլ",
|
|
42
|
+
"Մայիս",
|
|
43
|
+
"Հունիս",
|
|
44
|
+
"Հուլիս",
|
|
45
|
+
"Օգոստոս",
|
|
46
|
+
"Սեպտեմբեր",
|
|
47
|
+
"Հոկտեմբեր",
|
|
48
|
+
"Նոյեմբեր",
|
|
49
|
+
"Դեկտեմբեր",
|
|
50
|
+
];
|
|
51
|
+
/** Monday-first — override via `weekdayLabels` for other locales. */
|
|
52
|
+
const DEFAULT_WEEKDAY_LABELS = ["ԵՐ", "ԵՔ", "ՉՐ", "ՀՆ", "ՈՒՐ", "ՇԲ", "ԿՐ"];
|
|
53
|
+
|
|
54
|
+
export type DateRange = { start: Date | null; end: Date | null };
|
|
55
|
+
|
|
56
|
+
const EMPTY_RANGE: DateRange = { start: null, end: null };
|
|
57
|
+
|
|
58
|
+
type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
open?: boolean;
|
|
61
|
+
defaultOpen?: boolean;
|
|
62
|
+
onOpenChange?: (open: boolean) => void;
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* When true (default), the calendar closes after a complete selection —
|
|
66
|
+
* a single date, or both ends of a range. Set false to keep it open.
|
|
67
|
+
*/
|
|
68
|
+
closeOnSelect?: boolean;
|
|
69
|
+
minDate?: Date;
|
|
70
|
+
maxDate?: Date;
|
|
71
|
+
/** First visible month when the calendar opens; defaults to the selected value's month, else today. */
|
|
72
|
+
defaultVisibleMonth?: Date;
|
|
73
|
+
/** Monday-first full month names. Defaults to Armenian. */
|
|
74
|
+
monthNames?: string[];
|
|
75
|
+
/** Weekday abbreviations, ordered starting from `weekStartsOn`. Defaults to Armenian, Monday-first. */
|
|
76
|
+
weekdayLabels?: string[];
|
|
77
|
+
/** Formats a selected date for the trigger and day accessibility labels. Defaults to "DD.MM.YYYY". */
|
|
78
|
+
formatValue?: (date: Date) => string;
|
|
79
|
+
/** First day of the week column (0 = Sunday, 1 = Monday). Defaults to Monday. */
|
|
80
|
+
weekStartsOn?: 0 | 1;
|
|
81
|
+
style?: StyleProp<ViewStyle>;
|
|
82
|
+
className?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type DatePickerSingleProps = DatePickerBaseProps & {
|
|
86
|
+
range?: false;
|
|
87
|
+
value?: Date | null;
|
|
88
|
+
defaultValue?: Date | null;
|
|
89
|
+
onValueChange?: (value: Date | null) => void;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type DatePickerRangeProps = DatePickerBaseProps & {
|
|
93
|
+
range: true;
|
|
94
|
+
value?: DateRange;
|
|
95
|
+
defaultValue?: DateRange;
|
|
96
|
+
onValueChange?: (value: DateRange) => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
|
|
100
|
+
|
|
101
|
+
const DEFAULT_WIDTH = 380;
|
|
102
|
+
const TRIGGER_HEIGHT = 44;
|
|
103
|
+
const TRIGGER_RADIUS = 60;
|
|
104
|
+
const MENU_RADIUS = 20;
|
|
105
|
+
const ICON_SIZE = 20;
|
|
106
|
+
const NAV_ICON_SIZE = 20;
|
|
107
|
+
const DAY_CELL_HEIGHT = 44;
|
|
108
|
+
const DAY_CIRCLE_SIZE = 44;
|
|
109
|
+
|
|
110
|
+
export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>(
|
|
111
|
+
function DatePicker(props, forwardedRef) {
|
|
112
|
+
const {
|
|
113
|
+
placeholder = "Select date",
|
|
114
|
+
open: openProp,
|
|
115
|
+
defaultOpen = false,
|
|
116
|
+
onOpenChange,
|
|
117
|
+
disabled = false,
|
|
118
|
+
closeOnSelect = true,
|
|
119
|
+
minDate,
|
|
120
|
+
maxDate,
|
|
121
|
+
defaultVisibleMonth,
|
|
122
|
+
monthNames = DEFAULT_MONTH_NAMES,
|
|
123
|
+
weekdayLabels = DEFAULT_WEEKDAY_LABELS,
|
|
124
|
+
formatValue = formatDate,
|
|
125
|
+
weekStartsOn = 1,
|
|
126
|
+
style,
|
|
127
|
+
className,
|
|
128
|
+
accessibilityLabel,
|
|
129
|
+
// Pulled out only to keep them off `rest` (which is spread onto the root View);
|
|
130
|
+
// the real values are read from `props` directly below to preserve discriminated typing.
|
|
131
|
+
range: _range,
|
|
132
|
+
value: _value,
|
|
133
|
+
defaultValue: _defaultValue,
|
|
134
|
+
onValueChange: _onValueChange,
|
|
135
|
+
...rest
|
|
136
|
+
} = props;
|
|
137
|
+
|
|
138
|
+
const isRange = props.range === true;
|
|
139
|
+
const valueProp = props.value;
|
|
140
|
+
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
141
|
+
const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
|
|
142
|
+
const setRootRef = useMemo(() => mergeRefs(rootRef, forwardedRef), [forwardedRef]);
|
|
143
|
+
|
|
144
|
+
const isOpenControlled = typeof openProp === "boolean";
|
|
145
|
+
const isValueControlled = valueProp !== undefined;
|
|
146
|
+
|
|
147
|
+
const [uncontrolledSingle, setUncontrolledSingle] = useState<Date | null>(
|
|
148
|
+
!isRange ? ((props as DatePickerSingleProps).defaultValue ?? null) : null,
|
|
149
|
+
);
|
|
150
|
+
const [uncontrolledRange, setUncontrolledRange] = useState<DateRange>(
|
|
151
|
+
isRange ? ((props as DatePickerRangeProps).defaultValue ?? EMPTY_RANGE) : EMPTY_RANGE,
|
|
152
|
+
);
|
|
153
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
154
|
+
const [hoveredDayKey, setHoveredDayKey] = useState<number | null>(null);
|
|
155
|
+
|
|
156
|
+
const rangeValue: DateRange = isRange
|
|
157
|
+
? isValueControlled
|
|
158
|
+
? ((valueProp as DateRange) ?? EMPTY_RANGE)
|
|
159
|
+
: uncontrolledRange
|
|
160
|
+
: EMPTY_RANGE;
|
|
161
|
+
const singleValue: Date | null = !isRange
|
|
162
|
+
? isValueControlled
|
|
163
|
+
? ((valueProp as Date | null) ?? null)
|
|
164
|
+
: uncontrolledSingle
|
|
165
|
+
: null;
|
|
166
|
+
|
|
167
|
+
const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
|
|
168
|
+
|
|
169
|
+
const [visibleMonth, setVisibleMonth] = useState(() => {
|
|
170
|
+
const anchor =
|
|
171
|
+
defaultVisibleMonth ?? (isRange ? rangeValue.start : singleValue) ?? new Date();
|
|
172
|
+
return new Date(anchor.getFullYear(), anchor.getMonth(), 1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
useApplyWebClassName(rootRef, className);
|
|
176
|
+
|
|
177
|
+
const setOpen = useCallback(
|
|
178
|
+
(next: boolean) => {
|
|
179
|
+
if (!isOpenControlled) setUncontrolledOpen(next);
|
|
180
|
+
onOpenChange?.(next);
|
|
181
|
+
if (next) {
|
|
182
|
+
const anchor = (isRange ? rangeValue.start : singleValue) ?? new Date();
|
|
183
|
+
setVisibleMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1));
|
|
184
|
+
} else {
|
|
185
|
+
setHoveredDayKey(null);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
[isOpenControlled, isRange, onOpenChange, rangeValue.start, singleValue],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Close on outside click (web only; native has no document-level events).
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (Platform.OS !== "web" || !isOpen) return;
|
|
194
|
+
|
|
195
|
+
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
|
196
|
+
const root = rootRef.current as unknown as { contains?: (node: Node) => boolean } | null;
|
|
197
|
+
const target = event.target;
|
|
198
|
+
if (root?.contains && target instanceof Node && !root.contains(target)) {
|
|
199
|
+
setOpen(false);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
204
|
+
document.addEventListener("touchstart", handlePointerDown);
|
|
205
|
+
return () => {
|
|
206
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
207
|
+
document.removeEventListener("touchstart", handlePointerDown);
|
|
208
|
+
};
|
|
209
|
+
}, [isOpen, setOpen]);
|
|
210
|
+
|
|
211
|
+
const focusTrigger = useCallback(() => {
|
|
212
|
+
const node = triggerRef.current as unknown as { focus?: () => void } | null;
|
|
213
|
+
node?.focus?.();
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
const closeAndFocusTrigger = useCallback(() => {
|
|
217
|
+
setOpen(false);
|
|
218
|
+
focusTrigger();
|
|
219
|
+
}, [focusTrigger, setOpen]);
|
|
220
|
+
|
|
221
|
+
// Escape closes the calendar and returns focus to the trigger (web only).
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (Platform.OS !== "web" || !isOpen) return;
|
|
224
|
+
|
|
225
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
226
|
+
if (event.key === "Escape") {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
closeAndFocusTrigger();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
233
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
234
|
+
}, [isOpen, closeAndFocusTrigger]);
|
|
235
|
+
|
|
236
|
+
const handleTriggerPress = () => {
|
|
237
|
+
if (disabled) return;
|
|
238
|
+
setOpen(!isOpen);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const handleDayPress = useCallback(
|
|
242
|
+
(day: Date) => {
|
|
243
|
+
if (disabled || isDateDisabled(day, minDate, maxDate)) return;
|
|
244
|
+
const picked = startOfDay(day);
|
|
245
|
+
|
|
246
|
+
if (!isRange) {
|
|
247
|
+
if (!isValueControlled) setUncontrolledSingle(picked);
|
|
248
|
+
(props as DatePickerSingleProps).onValueChange?.(picked);
|
|
249
|
+
if (closeOnSelect) setOpen(false);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const { start, end } = rangeValue;
|
|
254
|
+
const startingNewRange = !start || (start && end);
|
|
255
|
+
const nextRange: DateRange = startingNewRange
|
|
256
|
+
? { start: picked, end: null }
|
|
257
|
+
: isBefore(picked, start)
|
|
258
|
+
? { start: picked, end: null }
|
|
259
|
+
: { start, end: picked };
|
|
260
|
+
|
|
261
|
+
if (!isValueControlled) setUncontrolledRange(nextRange);
|
|
262
|
+
(props as DatePickerRangeProps).onValueChange?.(nextRange);
|
|
263
|
+
if (closeOnSelect && nextRange.start && nextRange.end) setOpen(false);
|
|
264
|
+
},
|
|
265
|
+
[
|
|
266
|
+
closeOnSelect,
|
|
267
|
+
disabled,
|
|
268
|
+
isRange,
|
|
269
|
+
isValueControlled,
|
|
270
|
+
maxDate,
|
|
271
|
+
minDate,
|
|
272
|
+
props,
|
|
273
|
+
rangeValue,
|
|
274
|
+
setOpen,
|
|
275
|
+
],
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const handlePrevMonth = () => setVisibleMonth((current) => addMonths(current, -1));
|
|
279
|
+
const handleNextMonth = () => setVisibleMonth((current) => addMonths(current, 1));
|
|
280
|
+
|
|
281
|
+
const weeks = useMemo(
|
|
282
|
+
() => getMonthMatrix(visibleMonth, weekStartsOn),
|
|
283
|
+
[visibleMonth, weekStartsOn],
|
|
284
|
+
);
|
|
285
|
+
const today = useMemo(() => startOfDay(new Date()), []);
|
|
286
|
+
|
|
287
|
+
const hasValue = isRange ? rangeValue.start != null : singleValue != null;
|
|
288
|
+
const triggerLabel = isRange
|
|
289
|
+
? rangeValue.start && rangeValue.end
|
|
290
|
+
? `${formatValue(rangeValue.start)} - ${formatValue(rangeValue.end)}`
|
|
291
|
+
: rangeValue.start
|
|
292
|
+
? `${formatValue(rangeValue.start)} - ...`
|
|
293
|
+
: placeholder
|
|
294
|
+
: singleValue
|
|
295
|
+
? formatValue(singleValue)
|
|
296
|
+
: placeholder;
|
|
297
|
+
|
|
298
|
+
const triggerBorderColor = isOpen ? colors.primaryMuted : colors.grey700;
|
|
299
|
+
const triggerTextColor = hasValue ? colors.white : colors.grey100;
|
|
300
|
+
|
|
301
|
+
const resolvedAccessibilityLabel = accessibilityLabel ?? placeholder;
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<View
|
|
305
|
+
{...rest}
|
|
306
|
+
ref={setRootRef}
|
|
307
|
+
style={[styles.root, isOpen && styles.rootOpen, disabled && styles.disabled, style]}
|
|
308
|
+
accessibilityState={{ disabled, expanded: isOpen }}
|
|
309
|
+
>
|
|
310
|
+
<View style={[styles.triggerWrap, isOpen && styles.triggerWrapOpen]}>
|
|
311
|
+
<Pressable
|
|
312
|
+
ref={triggerRef}
|
|
313
|
+
onPress={handleTriggerPress}
|
|
314
|
+
disabled={disabled}
|
|
315
|
+
accessibilityRole="button"
|
|
316
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
317
|
+
accessibilityState={{ disabled, expanded: isOpen }}
|
|
318
|
+
style={[
|
|
319
|
+
styles.trigger,
|
|
320
|
+
{ borderColor: triggerBorderColor, backgroundColor: colors.grey700 },
|
|
321
|
+
]}
|
|
322
|
+
>
|
|
323
|
+
<Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
|
|
324
|
+
{triggerLabel}
|
|
325
|
+
</Text>
|
|
326
|
+
<View style={styles.iconSlot}>
|
|
327
|
+
<CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
|
|
328
|
+
</View>
|
|
329
|
+
</Pressable>
|
|
330
|
+
|
|
331
|
+
{isOpen ? (
|
|
332
|
+
<View style={styles.menu}>
|
|
333
|
+
<View style={styles.calendarHeader}>
|
|
334
|
+
<Pressable
|
|
335
|
+
onPress={handlePrevMonth}
|
|
336
|
+
hitSlop={8}
|
|
337
|
+
accessibilityRole="button"
|
|
338
|
+
accessibilityLabel="Previous month"
|
|
339
|
+
style={styles.navButton}
|
|
340
|
+
>
|
|
341
|
+
<ChevronLeftIcon size={NAV_ICON_SIZE} color={colors.white} />
|
|
342
|
+
</Pressable>
|
|
343
|
+
|
|
344
|
+
<Text style={styles.monthHeading}>
|
|
345
|
+
{monthNames[visibleMonth.getMonth()]} {visibleMonth.getFullYear()}
|
|
346
|
+
</Text>
|
|
347
|
+
|
|
348
|
+
<Pressable
|
|
349
|
+
onPress={handleNextMonth}
|
|
350
|
+
hitSlop={8}
|
|
351
|
+
accessibilityRole="button"
|
|
352
|
+
accessibilityLabel="Next month"
|
|
353
|
+
style={styles.navButton}
|
|
354
|
+
>
|
|
355
|
+
<View style={styles.navIconMirror}>
|
|
356
|
+
<ChevronLeftIcon size={NAV_ICON_SIZE} color={colors.white} />
|
|
357
|
+
</View>
|
|
358
|
+
</Pressable>
|
|
359
|
+
</View>
|
|
360
|
+
|
|
361
|
+
<View>
|
|
362
|
+
<View style={styles.weekdayRow}>
|
|
363
|
+
{weekdayLabels.map((weekdayLabel, index) => (
|
|
364
|
+
<View key={`${weekdayLabel}-${index}`} style={styles.dayCellWrap}>
|
|
365
|
+
<Text style={styles.weekdayText}>{weekdayLabel}</Text>
|
|
366
|
+
</View>
|
|
367
|
+
))}
|
|
368
|
+
</View>
|
|
369
|
+
|
|
370
|
+
{weeks.map((week, weekIndex) => {
|
|
371
|
+
let rangeHighlightStyle: { left: `${number}%`; width: `${number}%` } | null =
|
|
372
|
+
null;
|
|
373
|
+
|
|
374
|
+
if (isRange && rangeValue.start && rangeValue.end) {
|
|
375
|
+
const rangeIndices = week.reduce<number[]>((acc, day, index) => {
|
|
376
|
+
if (isWithinRange(day, rangeValue.start as Date, rangeValue.end as Date)) {
|
|
377
|
+
acc.push(index);
|
|
378
|
+
}
|
|
379
|
+
return acc;
|
|
380
|
+
}, []);
|
|
381
|
+
|
|
382
|
+
if (rangeIndices.length > 0) {
|
|
383
|
+
const first = rangeIndices[0];
|
|
384
|
+
const last = rangeIndices[rangeIndices.length - 1];
|
|
385
|
+
const startsHere = isSameDay(week[first], rangeValue.start);
|
|
386
|
+
const endsHere = isSameDay(week[last], rangeValue.end);
|
|
387
|
+
// Half a cell so the bar meets the circle center, not past it.
|
|
388
|
+
const cellPct = 100 / 7;
|
|
389
|
+
const halfCellPct = cellPct / 2;
|
|
390
|
+
let leftPct = first * cellPct;
|
|
391
|
+
let widthPct = (last - first + 1) * cellPct;
|
|
392
|
+
if (startsHere) {
|
|
393
|
+
leftPct += halfCellPct;
|
|
394
|
+
widthPct -= halfCellPct;
|
|
395
|
+
}
|
|
396
|
+
if (endsHere) {
|
|
397
|
+
widthPct -= halfCellPct;
|
|
398
|
+
}
|
|
399
|
+
// Same-day range: only the circle, no connector bar.
|
|
400
|
+
if (widthPct > 0) {
|
|
401
|
+
rangeHighlightStyle = {
|
|
402
|
+
left: `${leftPct}%`,
|
|
403
|
+
width: `${widthPct}%`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<View
|
|
411
|
+
key={weekIndex}
|
|
412
|
+
style={[styles.weekRow, weekIndex > 0 && styles.weekRowSpaced]}
|
|
413
|
+
>
|
|
414
|
+
{rangeHighlightStyle ? (
|
|
415
|
+
<View
|
|
416
|
+
pointerEvents="none"
|
|
417
|
+
style={[styles.rangeHighlight, rangeHighlightStyle]}
|
|
418
|
+
/>
|
|
419
|
+
) : null}
|
|
420
|
+
|
|
421
|
+
{week.map((day, dayIndex) => {
|
|
422
|
+
const inCurrentMonth = isSameMonth(day, visibleMonth);
|
|
423
|
+
const isToday = isSameDay(day, today);
|
|
424
|
+
const isSelectedSingle = !isRange && isSameDay(day, singleValue);
|
|
425
|
+
const isRangeStart = isRange && isSameDay(day, rangeValue.start);
|
|
426
|
+
const isRangeEnd = isRange && isSameDay(day, rangeValue.end);
|
|
427
|
+
const isSelectedEndpoint = isSelectedSingle || isRangeStart || isRangeEnd;
|
|
428
|
+
const dayDisabled = disabled || isDateDisabled(day, minDate, maxDate);
|
|
429
|
+
const dayKey = startOfDay(day).getTime();
|
|
430
|
+
const isHovered =
|
|
431
|
+
!dayDisabled && !isSelectedEndpoint && hoveredDayKey === dayKey;
|
|
432
|
+
|
|
433
|
+
const dayTextColor = isSelectedEndpoint
|
|
434
|
+
? colors.white
|
|
435
|
+
: dayDisabled
|
|
436
|
+
? colors.grey400
|
|
437
|
+
: isToday
|
|
438
|
+
? colors.primary
|
|
439
|
+
: !inCurrentMonth
|
|
440
|
+
? colors.grey300
|
|
441
|
+
: colors.white;
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<Pressable
|
|
445
|
+
key={dayIndex}
|
|
446
|
+
onPress={() => handleDayPress(day)}
|
|
447
|
+
disabled={dayDisabled}
|
|
448
|
+
onHoverIn={() => setHoveredDayKey(dayKey)}
|
|
449
|
+
onHoverOut={() =>
|
|
450
|
+
setHoveredDayKey((current) => (current === dayKey ? null : current))
|
|
451
|
+
}
|
|
452
|
+
onPressIn={() => setHoveredDayKey(dayKey)}
|
|
453
|
+
onPressOut={() =>
|
|
454
|
+
setHoveredDayKey((current) => (current === dayKey ? null : current))
|
|
455
|
+
}
|
|
456
|
+
style={styles.dayCellWrap}
|
|
457
|
+
accessibilityRole="button"
|
|
458
|
+
accessibilityLabel={formatValue(day)}
|
|
459
|
+
accessibilityState={{
|
|
460
|
+
disabled: dayDisabled,
|
|
461
|
+
selected: isSelectedEndpoint,
|
|
462
|
+
}}
|
|
463
|
+
>
|
|
464
|
+
<View
|
|
465
|
+
style={[
|
|
466
|
+
styles.dayCircle,
|
|
467
|
+
isSelectedEndpoint && styles.dayCircleSelected,
|
|
468
|
+
isHovered && styles.dayCircleHovered,
|
|
469
|
+
]}
|
|
470
|
+
>
|
|
471
|
+
<Text style={[styles.dayText, { color: dayTextColor }]}>
|
|
472
|
+
{day.getDate()}
|
|
473
|
+
</Text>
|
|
474
|
+
</View>
|
|
475
|
+
{isToday && !isSelectedEndpoint ? (
|
|
476
|
+
<View style={styles.todayDot} />
|
|
477
|
+
) : null}
|
|
478
|
+
</Pressable>
|
|
479
|
+
);
|
|
480
|
+
})}
|
|
481
|
+
</View>
|
|
482
|
+
);
|
|
483
|
+
})}
|
|
484
|
+
</View>
|
|
485
|
+
</View>
|
|
486
|
+
) : null}
|
|
487
|
+
</View>
|
|
488
|
+
</View>
|
|
489
|
+
);
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const styles = StyleSheet.create({
|
|
494
|
+
root: {
|
|
495
|
+
width: DEFAULT_WIDTH,
|
|
496
|
+
gap: 8,
|
|
497
|
+
alignSelf: "flex-start",
|
|
498
|
+
},
|
|
499
|
+
rootOpen: {
|
|
500
|
+
zIndex: 30,
|
|
501
|
+
},
|
|
502
|
+
disabled: {
|
|
503
|
+
opacity: 0.6,
|
|
504
|
+
},
|
|
505
|
+
triggerWrap: {
|
|
506
|
+
position: "relative",
|
|
507
|
+
zIndex: 1,
|
|
508
|
+
},
|
|
509
|
+
triggerWrapOpen: {
|
|
510
|
+
zIndex: 20,
|
|
511
|
+
},
|
|
512
|
+
trigger: {
|
|
513
|
+
height: TRIGGER_HEIGHT,
|
|
514
|
+
borderRadius: TRIGGER_RADIUS,
|
|
515
|
+
borderWidth: 1,
|
|
516
|
+
paddingVertical: 10,
|
|
517
|
+
paddingHorizontal: 16,
|
|
518
|
+
flexDirection: "row",
|
|
519
|
+
alignItems: "center",
|
|
520
|
+
gap: 10,
|
|
521
|
+
overflow: "hidden",
|
|
522
|
+
},
|
|
523
|
+
triggerText: {
|
|
524
|
+
...datePickerTypography.field,
|
|
525
|
+
flex: 1,
|
|
526
|
+
minWidth: 0,
|
|
527
|
+
},
|
|
528
|
+
iconSlot: {
|
|
529
|
+
width: ICON_SIZE,
|
|
530
|
+
height: ICON_SIZE,
|
|
531
|
+
alignItems: "center",
|
|
532
|
+
justifyContent: "center",
|
|
533
|
+
flexShrink: 0,
|
|
534
|
+
},
|
|
535
|
+
menu: {
|
|
536
|
+
position: "absolute",
|
|
537
|
+
top: "100%",
|
|
538
|
+
left: 0,
|
|
539
|
+
right: 0,
|
|
540
|
+
marginTop: 8,
|
|
541
|
+
zIndex: 10,
|
|
542
|
+
borderRadius: MENU_RADIUS,
|
|
543
|
+
padding: 12,
|
|
544
|
+
gap: 0,
|
|
545
|
+
backgroundColor: colors.grey700,
|
|
546
|
+
...Platform.select({
|
|
547
|
+
web: {
|
|
548
|
+
boxShadow: `0 8px 24px ${colors.black}73`,
|
|
549
|
+
},
|
|
550
|
+
default: {
|
|
551
|
+
elevation: 8,
|
|
552
|
+
},
|
|
553
|
+
}),
|
|
554
|
+
},
|
|
555
|
+
calendarHeader: {
|
|
556
|
+
height: 36,
|
|
557
|
+
flexDirection: "row",
|
|
558
|
+
alignItems: "center",
|
|
559
|
+
justifyContent: "space-between",
|
|
560
|
+
},
|
|
561
|
+
navButton: {
|
|
562
|
+
width: 36,
|
|
563
|
+
height: 36,
|
|
564
|
+
alignItems: "center",
|
|
565
|
+
justifyContent: "center",
|
|
566
|
+
},
|
|
567
|
+
navIconMirror: {
|
|
568
|
+
transform: [{ scaleX: -1 }],
|
|
569
|
+
},
|
|
570
|
+
monthHeading: {
|
|
571
|
+
...datePickerTypography.monthHeading,
|
|
572
|
+
color: colors.white,
|
|
573
|
+
textAlign: "center",
|
|
574
|
+
},
|
|
575
|
+
weekdayRow: {
|
|
576
|
+
flexDirection: "row",
|
|
577
|
+
},
|
|
578
|
+
weekdayText: {
|
|
579
|
+
...datePickerTypography.weekday,
|
|
580
|
+
color: colors.grey300,
|
|
581
|
+
textAlign: "center",
|
|
582
|
+
textTransform: "uppercase",
|
|
583
|
+
},
|
|
584
|
+
weekRow: {
|
|
585
|
+
flexDirection: "row",
|
|
586
|
+
position: "relative",
|
|
587
|
+
},
|
|
588
|
+
weekRowSpaced: {
|
|
589
|
+
marginTop: 8,
|
|
590
|
+
},
|
|
591
|
+
rangeHighlight: {
|
|
592
|
+
position: "absolute",
|
|
593
|
+
top: (DAY_CELL_HEIGHT - DAY_CIRCLE_SIZE) / 2,
|
|
594
|
+
bottom: (DAY_CELL_HEIGHT - DAY_CIRCLE_SIZE) / 2,
|
|
595
|
+
borderRadius: DAY_CIRCLE_SIZE / 2,
|
|
596
|
+
backgroundColor: colors.primaryHover,
|
|
597
|
+
},
|
|
598
|
+
dayCellWrap: {
|
|
599
|
+
flex: 1,
|
|
600
|
+
height: DAY_CELL_HEIGHT,
|
|
601
|
+
alignItems: "center",
|
|
602
|
+
justifyContent: "center",
|
|
603
|
+
},
|
|
604
|
+
dayCircle: {
|
|
605
|
+
width: DAY_CIRCLE_SIZE,
|
|
606
|
+
height: DAY_CIRCLE_SIZE,
|
|
607
|
+
borderRadius: DAY_CIRCLE_SIZE / 2,
|
|
608
|
+
alignItems: "center",
|
|
609
|
+
justifyContent: "center",
|
|
610
|
+
},
|
|
611
|
+
dayCircleSelected: {
|
|
612
|
+
backgroundColor: colors.primary,
|
|
613
|
+
},
|
|
614
|
+
dayCircleHovered: {
|
|
615
|
+
backgroundColor: colors.grey600,
|
|
616
|
+
},
|
|
617
|
+
dayText: {
|
|
618
|
+
...datePickerTypography.day,
|
|
619
|
+
},
|
|
620
|
+
todayDot: {
|
|
621
|
+
position: "absolute",
|
|
622
|
+
bottom: 2,
|
|
623
|
+
width: 4,
|
|
624
|
+
height: 4,
|
|
625
|
+
borderRadius: 2,
|
|
626
|
+
backgroundColor: colors.primary,
|
|
627
|
+
},
|
|
628
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -7,6 +7,14 @@ export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button";
|
|
|
7
7
|
export { Checkbox } from "./Checkbox";
|
|
8
8
|
export type { CheckboxProps, CheckboxVariant } from "./Checkbox";
|
|
9
9
|
|
|
10
|
+
export { DatePicker } from "./DatePicker";
|
|
11
|
+
export type {
|
|
12
|
+
DatePickerProps,
|
|
13
|
+
DatePickerSingleProps,
|
|
14
|
+
DatePickerRangeProps,
|
|
15
|
+
DateRange,
|
|
16
|
+
} from "./DatePicker";
|
|
17
|
+
|
|
10
18
|
export { Input } from "./Input";
|
|
11
19
|
export type { InputProps, InputSize } from "./Input";
|
|
12
20
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Svg, { Path } from "react-native-svg";
|
|
2
|
+
import { CALENDAR_OUTLINE_PATH } from "./calendarOutlinePath";
|
|
3
|
+
import { colors } from "../../theme";
|
|
4
|
+
import type { IconProps } from "../types";
|
|
5
|
+
|
|
6
|
+
export { CALENDAR_OUTLINE_PATH } from "./calendarOutlinePath";
|
|
7
|
+
|
|
8
|
+
/** Native — react-native-svg (peer dependency). */
|
|
9
|
+
export function CalendarOutlineIcon({
|
|
10
|
+
size = 20,
|
|
11
|
+
color = colors.primary,
|
|
12
|
+
strokeWidth = 1.5,
|
|
13
|
+
}: IconProps) {
|
|
14
|
+
return (
|
|
15
|
+
<Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
16
|
+
<Path
|
|
17
|
+
d={CALENDAR_OUTLINE_PATH}
|
|
18
|
+
stroke={color}
|
|
19
|
+
strokeWidth={strokeWidth}
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
/>
|
|
23
|
+
</Svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CALENDAR_OUTLINE_PATH } from "./calendarOutlinePath";
|
|
2
|
+
import { colors } from "../../theme";
|
|
3
|
+
import type { IconProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/** Web / Storybook — plain SVG (no react-native-svg). */
|
|
6
|
+
export function CalendarOutlineIcon({
|
|
7
|
+
size = 20,
|
|
8
|
+
color = colors.primary,
|
|
9
|
+
strokeWidth = 1.5,
|
|
10
|
+
}: IconProps) {
|
|
11
|
+
return (
|
|
12
|
+
<svg
|
|
13
|
+
width={size}
|
|
14
|
+
height={size}
|
|
15
|
+
viewBox="0 0 16 16"
|
|
16
|
+
fill="none"
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
aria-hidden
|
|
19
|
+
>
|
|
20
|
+
<path
|
|
21
|
+
d={CALENDAR_OUTLINE_PATH}
|
|
22
|
+
stroke={color}
|
|
23
|
+
strokeWidth={strokeWidth}
|
|
24
|
+
strokeLinecap="round"
|
|
25
|
+
strokeLinejoin="round"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { CALENDAR_OUTLINE_PATH } from "./calendarOutlinePath";
|