@scripso-homepad/ui 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WDSMJWKC.js → chunk-66WR57JJ.js} +35 -3
- package/dist/chunk-66WR57JJ.js.map +1 -0
- package/dist/index.cjs +2260 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -1
- package/dist/index.d.ts +133 -1
- package/dist/index.js +1960 -97
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +30650 -1009
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.css +78 -0
- package/dist/web/index.css.map +1 -0
- package/dist/web/index.d.cts +267 -6
- package/dist/web/index.d.ts +267 -6
- package/dist/web/index.js +30179 -601
- package/dist/web/index.js.map +1 -1
- package/package.json +5 -4
- package/src/components/Calendar.stories.tsx +337 -0
- package/src/components/Calendar.tsx +441 -0
- package/src/components/DatePicker.stories.tsx +219 -0
- package/src/components/DatePicker.tsx +366 -0
- package/src/components/Select.stories.tsx +134 -0
- package/src/components/Select.tsx +462 -0
- package/src/css.d.ts +1 -0
- package/src/icons/CalendarIcon.tsx +28 -0
- package/src/icons/CalendarIcon.web.tsx +34 -0
- package/src/icons/ChevronDownIcon.tsx +11 -4
- package/src/icons/ChevronDownIcon.web.tsx +6 -4
- package/src/icons/ChevronNavIcons.tsx +44 -0
- package/src/icons/ChevronNavIcons.web.tsx +57 -0
- package/src/icons/calendarIconPaths.ts +2 -0
- package/src/icons/chevronDownPath.ts +1 -0
- package/src/icons/chevronNavPaths.ts +2 -0
- package/src/index.ts +41 -0
- package/src/theme/calendar.ts +357 -0
- package/src/theme/select.ts +249 -0
- package/src/utils/calendarDays.ts +273 -0
- package/src/utils/calendarLocaleContext.tsx +95 -0
- package/src/utils/calendarLocalization.ts +187 -0
- package/src/utils/countryDropdownScrollStyles.ts +53 -13
- package/src/utils/formatMultiSelectDisplay.ts +56 -0
- package/src/web/components/Badge.stories.tsx +43 -0
- package/src/web/components/Badge.tsx +35 -0
- package/src/web/components/Pagination.stories.tsx +78 -0
- package/src/web/components/Pagination.tsx +153 -0
- package/src/web/components/StatusBadge.tsx +20 -0
- package/src/web/components/Table.stories.tsx +415 -0
- package/src/web/components/Table.tsx +3 -0
- package/src/web/components/TableActionButton.tsx +67 -0
- package/src/web/components/TableActionsMenu.tsx +208 -0
- package/src/web/components/TableIconText.tsx +23 -0
- package/src/web/components/TableRowStatusActions.tsx +33 -0
- package/src/web/components/TableStatusActionsCell.tsx +29 -0
- package/src/web/components/pagination-utils.ts +39 -0
- package/src/web/components/table/DataTable.tsx +123 -0
- package/src/web/components/table/TablePrimitives.tsx +214 -0
- package/src/web/components/table/TableScrollArea.tsx +51 -0
- package/src/web/components/table/constants.ts +59 -0
- package/src/web/components/table/data-table-class-names.ts +26 -0
- package/src/web/components/table/index.ts +44 -0
- package/src/web/components/table/table-row-context.tsx +19 -0
- package/src/web/components/table/use-horizontal-scroll-state.ts +72 -0
- package/src/web/components/table-scroll.css +81 -0
- package/src/web/hooks/useOnClickOutside.ts +20 -5
- package/src/web/icons/TableMenuActivateIcon.tsx +27 -0
- package/src/web/icons/TableMenuCheckIcon.tsx +27 -0
- package/src/web/icons/TableMenuCloseIcon.tsx +27 -0
- package/src/web/icons/TableMenuEditIcon.tsx +27 -0
- package/src/web/icons/TableMenuInfoIcon.tsx +27 -0
- package/src/web/icons/TableMenuRefreshIcon.tsx +27 -0
- package/src/web/icons/TableMenuSuspendIcon.tsx +34 -0
- package/src/web/icons/TableMenuTrashIcon.tsx +27 -0
- package/src/web/icons/TableMoreIcon.tsx +41 -0
- package/src/web/icons/TableSortIcon.tsx +37 -0
- package/src/web/index.ts +74 -0
- package/src/web/layout/AppHeader.stories.tsx +2 -0
- package/src/web/layout/AppHeader.tsx +18 -14
- package/src/web/layout/DashboardLayout.stories.tsx +1 -0
- package/src/web/layout/DashboardLayout.tsx +4 -4
- package/src/web/layout/story-helpers.tsx +10 -2
- package/dist/chunk-WDSMJWKC.js.map +0 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { useMemo, useRef, useState, type ComponentRef } from "react";
|
|
2
|
+
import { Pressable, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "../icons/ChevronNavIcons";
|
|
4
|
+
import {
|
|
5
|
+
CALENDAR_NAV_ICON_COLOR,
|
|
6
|
+
CALENDAR_NAV_ICON_SIZE,
|
|
7
|
+
CALENDAR_YEARS_PER_PAGE,
|
|
8
|
+
calendarDropdownMetrics,
|
|
9
|
+
} from "../theme/calendar";
|
|
10
|
+
import {
|
|
11
|
+
addMonths,
|
|
12
|
+
buildCalendarDays,
|
|
13
|
+
formatCalendarYear,
|
|
14
|
+
getYearPageStart,
|
|
15
|
+
isMonthDisabled,
|
|
16
|
+
isYearDisabled,
|
|
17
|
+
selectRangeDate,
|
|
18
|
+
type CalendarDayCell,
|
|
19
|
+
type DateRange,
|
|
20
|
+
} from "../utils/calendarDays";
|
|
21
|
+
import {
|
|
22
|
+
formatLocalizedCalendarMonthYear,
|
|
23
|
+
getCalendarMonthLabels,
|
|
24
|
+
getCalendarTranslations,
|
|
25
|
+
resolveWeekdayLabels,
|
|
26
|
+
type CalendarTranslations,
|
|
27
|
+
} from "../utils/calendarLocalization";
|
|
28
|
+
import { useCalendarLocale } from "../utils/calendarLocaleContext";
|
|
29
|
+
import { useApplyWebClassName } from "../utils/useApplyWebClassName";
|
|
30
|
+
|
|
31
|
+
export type { DateRange };
|
|
32
|
+
|
|
33
|
+
export type CalendarLabels = Partial<Pick<CalendarTranslations, CalendarTranslationKey>> & {
|
|
34
|
+
weekdays?: string[];
|
|
35
|
+
formatMonthYear?: (date: Date) => string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type CalendarTranslationKey =
|
|
39
|
+
| "previousMonth"
|
|
40
|
+
| "nextMonth"
|
|
41
|
+
| "previousYear"
|
|
42
|
+
| "nextYear"
|
|
43
|
+
| "previousYears"
|
|
44
|
+
| "nextYears"
|
|
45
|
+
| "selectMonth"
|
|
46
|
+
| "selectYear"
|
|
47
|
+
| "closeCalendar";
|
|
48
|
+
|
|
49
|
+
type CalendarPickerView = "days" | "months" | "years";
|
|
50
|
+
|
|
51
|
+
type CalendarBaseProps = {
|
|
52
|
+
viewDate?: Date;
|
|
53
|
+
defaultViewDate?: Date;
|
|
54
|
+
onViewDateChange?: (date: Date) => void;
|
|
55
|
+
locale?: string;
|
|
56
|
+
labels?: CalendarLabels;
|
|
57
|
+
minDate?: Date;
|
|
58
|
+
maxDate?: Date;
|
|
59
|
+
today?: Date;
|
|
60
|
+
style?: StyleProp<ViewStyle>;
|
|
61
|
+
className?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type CalendarSingleProps = CalendarBaseProps & {
|
|
65
|
+
mode?: "single";
|
|
66
|
+
value?: Date;
|
|
67
|
+
onChange?: (date: Date) => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type CalendarRangeProps = CalendarBaseProps & {
|
|
71
|
+
mode: "range";
|
|
72
|
+
value?: DateRange;
|
|
73
|
+
onChange?: (range: DateRange) => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type CalendarProps = CalendarSingleProps | CalendarRangeProps;
|
|
77
|
+
|
|
78
|
+
function resolveDayCellStyle(day: CalendarDayCell): StyleProp<ViewStyle> {
|
|
79
|
+
if (day.isDisabled) {
|
|
80
|
+
return [calendarDropdownMetrics.dayCell];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (day.isSelected) {
|
|
84
|
+
return [calendarDropdownMetrics.dayCell, calendarDropdownMetrics.dayCellSelected];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
calendarDropdownMetrics.dayCell,
|
|
89
|
+
day.inCurrentMonth && day.isPast && !day.isToday
|
|
90
|
+
? calendarDropdownMetrics.dayCellPast
|
|
91
|
+
: null,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveDayLabelStyle(day: CalendarDayCell): StyleProp<TextStyle> {
|
|
96
|
+
if (day.isDisabled) {
|
|
97
|
+
return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelDisabled];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (day.isSelected) {
|
|
101
|
+
return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelSelected];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!day.inCurrentMonth) {
|
|
105
|
+
return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelOutsideMonth];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (day.isToday) {
|
|
109
|
+
return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelToday];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (day.isPast) {
|
|
113
|
+
return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelMuted];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelFuture];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function Calendar(props: CalendarProps) {
|
|
120
|
+
const rangeProps = props.mode === "range" ? (props as CalendarRangeProps) : null;
|
|
121
|
+
const singleProps = props.mode === "range" ? null : (props as CalendarSingleProps);
|
|
122
|
+
const {
|
|
123
|
+
viewDate,
|
|
124
|
+
defaultViewDate,
|
|
125
|
+
onViewDateChange,
|
|
126
|
+
locale,
|
|
127
|
+
labels,
|
|
128
|
+
minDate,
|
|
129
|
+
maxDate,
|
|
130
|
+
today = new Date(),
|
|
131
|
+
style,
|
|
132
|
+
className,
|
|
133
|
+
} = props;
|
|
134
|
+
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
135
|
+
const [internalViewDate, setInternalViewDate] = useState(() => {
|
|
136
|
+
if (defaultViewDate) {
|
|
137
|
+
return defaultViewDate;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (rangeProps) {
|
|
141
|
+
return rangeProps.value?.start ?? today;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return singleProps?.value ?? today;
|
|
145
|
+
});
|
|
146
|
+
const activeViewDate = viewDate ?? internalViewDate;
|
|
147
|
+
const [pickerView, setPickerView] = useState<CalendarPickerView>("days");
|
|
148
|
+
const [pickerYear, setPickerYear] = useState(() => activeViewDate.getFullYear());
|
|
149
|
+
const [yearPageStart, setYearPageStart] = useState(() =>
|
|
150
|
+
getYearPageStart(activeViewDate.getFullYear(), CALENDAR_YEARS_PER_PAGE),
|
|
151
|
+
);
|
|
152
|
+
const customWeekdayLabels = labels?.weekdays;
|
|
153
|
+
const formatMonthYearLabel = labels?.formatMonthYear;
|
|
154
|
+
const resolvedLocale = useCalendarLocale(locale);
|
|
155
|
+
const translations = useMemo(
|
|
156
|
+
() => getCalendarTranslations(resolvedLocale),
|
|
157
|
+
[resolvedLocale],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
useApplyWebClassName(rootRef, className);
|
|
161
|
+
|
|
162
|
+
const weekdayLabels = useMemo(
|
|
163
|
+
() => customWeekdayLabels ?? resolveWeekdayLabels(resolvedLocale),
|
|
164
|
+
[customWeekdayLabels, resolvedLocale],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const monthLabels = useMemo(() => getCalendarMonthLabels(resolvedLocale), [resolvedLocale]);
|
|
168
|
+
|
|
169
|
+
const monthYearLabel = useMemo(() => {
|
|
170
|
+
if (formatMonthYearLabel) {
|
|
171
|
+
return formatMonthYearLabel(activeViewDate);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return formatLocalizedCalendarMonthYear(activeViewDate, resolvedLocale);
|
|
175
|
+
}, [activeViewDate, formatMonthYearLabel, resolvedLocale]);
|
|
176
|
+
|
|
177
|
+
const headerLabel = useMemo(() => {
|
|
178
|
+
if (pickerView === "months") {
|
|
179
|
+
return formatCalendarYear(pickerYear, resolvedLocale);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (pickerView === "years") {
|
|
183
|
+
const endYear = yearPageStart + CALENDAR_YEARS_PER_PAGE - 1;
|
|
184
|
+
return `${yearPageStart} – ${endYear}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return monthYearLabel;
|
|
188
|
+
}, [monthYearLabel, pickerView, pickerYear, resolvedLocale, yearPageStart]);
|
|
189
|
+
|
|
190
|
+
const days = useMemo(
|
|
191
|
+
() =>
|
|
192
|
+
buildCalendarDays({
|
|
193
|
+
viewDate: activeViewDate,
|
|
194
|
+
selectedDate: singleProps?.value,
|
|
195
|
+
selectedRange: rangeProps?.value,
|
|
196
|
+
today,
|
|
197
|
+
minDate,
|
|
198
|
+
maxDate,
|
|
199
|
+
}),
|
|
200
|
+
[activeViewDate, maxDate, minDate, rangeProps?.value, singleProps?.value, today],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const yearOptions = useMemo(
|
|
204
|
+
() =>
|
|
205
|
+
Array.from({ length: CALENDAR_YEARS_PER_PAGE }, (_, index) => yearPageStart + index),
|
|
206
|
+
[yearPageStart],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
function getLabel(key: CalendarTranslationKey): string {
|
|
210
|
+
return labels?.[key] ?? translations[key];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function setViewDate(nextDate: Date) {
|
|
214
|
+
if (viewDate === undefined) {
|
|
215
|
+
setInternalViewDate(nextDate);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onViewDateChange?.(nextDate);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handlePrevious() {
|
|
222
|
+
if (pickerView === "months") {
|
|
223
|
+
setPickerYear((year) => year - 1);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (pickerView === "years") {
|
|
228
|
+
setYearPageStart((start) => start - CALENDAR_YEARS_PER_PAGE);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setViewDate(addMonths(activeViewDate, -1));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleNext() {
|
|
236
|
+
if (pickerView === "months") {
|
|
237
|
+
setPickerYear((year) => year + 1);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (pickerView === "years") {
|
|
242
|
+
setYearPageStart((start) => start + CALENDAR_YEARS_PER_PAGE);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setViewDate(addMonths(activeViewDate, 1));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function handleHeaderPress() {
|
|
250
|
+
if (pickerView === "days") {
|
|
251
|
+
setPickerYear(activeViewDate.getFullYear());
|
|
252
|
+
setPickerView("months");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (pickerView === "months") {
|
|
257
|
+
setYearPageStart(getYearPageStart(pickerYear, CALENDAR_YEARS_PER_PAGE));
|
|
258
|
+
setPickerView("years");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleMonthSelect(month: number) {
|
|
263
|
+
setViewDate(new Date(pickerYear, month, 1));
|
|
264
|
+
setPickerView("days");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function handleYearSelect(year: number) {
|
|
268
|
+
setPickerYear(year);
|
|
269
|
+
setPickerView("months");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function handleDayPress(date: Date) {
|
|
273
|
+
if (rangeProps) {
|
|
274
|
+
rangeProps.onChange?.(selectRangeDate(date, rangeProps.value));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
singleProps?.onChange?.(date);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const headerIsPressable = pickerView === "days" || pickerView === "months";
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<View ref={rootRef} style={[calendarDropdownMetrics.panel, style]}>
|
|
285
|
+
<View style={calendarDropdownMetrics.header}>
|
|
286
|
+
<Pressable
|
|
287
|
+
accessibilityRole="button"
|
|
288
|
+
accessibilityLabel={
|
|
289
|
+
pickerView === "months"
|
|
290
|
+
? getLabel("previousYear")
|
|
291
|
+
: pickerView === "years"
|
|
292
|
+
? getLabel("previousYears")
|
|
293
|
+
: getLabel("previousMonth")
|
|
294
|
+
}
|
|
295
|
+
onPress={handlePrevious}
|
|
296
|
+
style={calendarDropdownMetrics.navButton}
|
|
297
|
+
>
|
|
298
|
+
<ChevronLeftIcon size={CALENDAR_NAV_ICON_SIZE} color={CALENDAR_NAV_ICON_COLOR} />
|
|
299
|
+
</Pressable>
|
|
300
|
+
|
|
301
|
+
{headerIsPressable ? (
|
|
302
|
+
<Pressable
|
|
303
|
+
accessibilityRole="button"
|
|
304
|
+
accessibilityLabel={
|
|
305
|
+
pickerView === "days" ? getLabel("selectMonth") : getLabel("selectYear")
|
|
306
|
+
}
|
|
307
|
+
onPress={handleHeaderPress}
|
|
308
|
+
style={calendarDropdownMetrics.headerTitlePressable}
|
|
309
|
+
>
|
|
310
|
+
<Text style={calendarDropdownMetrics.headerTitle}>{headerLabel}</Text>
|
|
311
|
+
</Pressable>
|
|
312
|
+
) : (
|
|
313
|
+
<View style={calendarDropdownMetrics.headerTitlePressable}>
|
|
314
|
+
<Text style={calendarDropdownMetrics.headerTitle}>{headerLabel}</Text>
|
|
315
|
+
</View>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
<Pressable
|
|
319
|
+
accessibilityRole="button"
|
|
320
|
+
accessibilityLabel={
|
|
321
|
+
pickerView === "months"
|
|
322
|
+
? getLabel("nextYear")
|
|
323
|
+
: pickerView === "years"
|
|
324
|
+
? getLabel("nextYears")
|
|
325
|
+
: getLabel("nextMonth")
|
|
326
|
+
}
|
|
327
|
+
onPress={handleNext}
|
|
328
|
+
style={calendarDropdownMetrics.navButton}
|
|
329
|
+
>
|
|
330
|
+
<ChevronRightIcon size={CALENDAR_NAV_ICON_SIZE} color={CALENDAR_NAV_ICON_COLOR} />
|
|
331
|
+
</Pressable>
|
|
332
|
+
</View>
|
|
333
|
+
|
|
334
|
+
{pickerView === "days" ? (
|
|
335
|
+
<>
|
|
336
|
+
<View style={calendarDropdownMetrics.weekdayRow}>
|
|
337
|
+
{weekdayLabels.map((weekday) => (
|
|
338
|
+
<View key={weekday} style={calendarDropdownMetrics.weekdayCell}>
|
|
339
|
+
<Text style={calendarDropdownMetrics.weekdayLabel}>{weekday}</Text>
|
|
340
|
+
</View>
|
|
341
|
+
))}
|
|
342
|
+
</View>
|
|
343
|
+
|
|
344
|
+
<View style={calendarDropdownMetrics.dayGrid}>
|
|
345
|
+
{days.map((day) => (
|
|
346
|
+
<Pressable
|
|
347
|
+
key={day.date.toISOString()}
|
|
348
|
+
accessibilityRole="button"
|
|
349
|
+
accessibilityState={{
|
|
350
|
+
selected: day.isSelected,
|
|
351
|
+
disabled: day.isDisabled,
|
|
352
|
+
}}
|
|
353
|
+
disabled={day.isDisabled}
|
|
354
|
+
onPress={() => handleDayPress(day.date)}
|
|
355
|
+
style={resolveDayCellStyle(day)}
|
|
356
|
+
>
|
|
357
|
+
<Text style={resolveDayLabelStyle(day)}>{day.date.getDate()}</Text>
|
|
358
|
+
{day.isToday && day.inCurrentMonth && !day.isSelected ? (
|
|
359
|
+
<View style={calendarDropdownMetrics.todayDot} />
|
|
360
|
+
) : null}
|
|
361
|
+
</Pressable>
|
|
362
|
+
))}
|
|
363
|
+
</View>
|
|
364
|
+
</>
|
|
365
|
+
) : (
|
|
366
|
+
<View style={calendarDropdownMetrics.pickerBody}>
|
|
367
|
+
<View style={calendarDropdownMetrics.pickerGrid}>
|
|
368
|
+
{pickerView === "months"
|
|
369
|
+
? monthLabels.map((monthLabel, month) => {
|
|
370
|
+
const isSelected =
|
|
371
|
+
activeViewDate.getFullYear() === pickerYear &&
|
|
372
|
+
activeViewDate.getMonth() === month;
|
|
373
|
+
const isCurrent =
|
|
374
|
+
today.getFullYear() === pickerYear && today.getMonth() === month;
|
|
375
|
+
const disabled = isMonthDisabled(pickerYear, month, minDate, maxDate);
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<Pressable
|
|
379
|
+
key={monthLabel}
|
|
380
|
+
accessibilityRole="button"
|
|
381
|
+
accessibilityState={{ selected: isSelected, disabled }}
|
|
382
|
+
disabled={disabled}
|
|
383
|
+
onPress={() => handleMonthSelect(month)}
|
|
384
|
+
style={[
|
|
385
|
+
calendarDropdownMetrics.pickerCell,
|
|
386
|
+
isSelected ? calendarDropdownMetrics.pickerCellSelected : null,
|
|
387
|
+
]}
|
|
388
|
+
>
|
|
389
|
+
<Text
|
|
390
|
+
style={[
|
|
391
|
+
calendarDropdownMetrics.pickerLabel,
|
|
392
|
+
isSelected ? calendarDropdownMetrics.pickerLabelSelected : null,
|
|
393
|
+
!isSelected && isCurrent
|
|
394
|
+
? calendarDropdownMetrics.pickerLabelCurrent
|
|
395
|
+
: null,
|
|
396
|
+
disabled ? calendarDropdownMetrics.pickerLabelDisabled : null,
|
|
397
|
+
]}
|
|
398
|
+
>
|
|
399
|
+
{monthLabel}
|
|
400
|
+
</Text>
|
|
401
|
+
</Pressable>
|
|
402
|
+
);
|
|
403
|
+
})
|
|
404
|
+
: yearOptions.map((year) => {
|
|
405
|
+
const isSelected = activeViewDate.getFullYear() === year;
|
|
406
|
+
const isCurrent = today.getFullYear() === year;
|
|
407
|
+
const disabled = isYearDisabled(year, minDate, maxDate);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<Pressable
|
|
411
|
+
key={year}
|
|
412
|
+
accessibilityRole="button"
|
|
413
|
+
accessibilityState={{ selected: isSelected, disabled }}
|
|
414
|
+
disabled={disabled}
|
|
415
|
+
onPress={() => handleYearSelect(year)}
|
|
416
|
+
style={[
|
|
417
|
+
calendarDropdownMetrics.pickerCell,
|
|
418
|
+
isSelected ? calendarDropdownMetrics.pickerCellSelected : null,
|
|
419
|
+
]}
|
|
420
|
+
>
|
|
421
|
+
<Text
|
|
422
|
+
style={[
|
|
423
|
+
calendarDropdownMetrics.pickerLabel,
|
|
424
|
+
isSelected ? calendarDropdownMetrics.pickerLabelSelected : null,
|
|
425
|
+
!isSelected && isCurrent
|
|
426
|
+
? calendarDropdownMetrics.pickerLabelCurrent
|
|
427
|
+
: null,
|
|
428
|
+
disabled ? calendarDropdownMetrics.pickerLabelDisabled : null,
|
|
429
|
+
]}
|
|
430
|
+
>
|
|
431
|
+
{year}
|
|
432
|
+
</Text>
|
|
433
|
+
</Pressable>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
436
|
+
</View>
|
|
437
|
+
</View>
|
|
438
|
+
)}
|
|
439
|
+
</View>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { View, StyleSheet } from "react-native";
|
|
4
|
+
import { formatLocalizedCalendarMonthYear } from "../utils/calendarLocalization";
|
|
5
|
+
import { DatePicker, type CalendarLabels, type DatePickerRangeProps, type DatePickerSingleProps, type DateRange } from "./DatePicker";
|
|
6
|
+
|
|
7
|
+
const figmaViewDate = new Date(2026, 5, 1);
|
|
8
|
+
const figmaSampleDate = new Date(2026, 5, 25);
|
|
9
|
+
|
|
10
|
+
function getCurrentMonthStart() {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
return new Date(now.getFullYear(), now.getMonth(), 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSampleRange(): DateRange {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
return {
|
|
18
|
+
start: new Date(now.getFullYear(), now.getMonth(), 20),
|
|
19
|
+
end: new Date(now.getFullYear(), now.getMonth(), 28),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const figmaLabels: CalendarLabels = {
|
|
24
|
+
weekdays: ["ԵՐ", "ԵՔ", "ՉՐ", "ՀՆ", "ՈՒՐ", "ՇԲ", "ԿՐ"],
|
|
25
|
+
formatMonthYear: (date) => formatLocalizedCalendarMonthYear(date, "hy-AM"),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const sharedDatePickerArgs = {
|
|
29
|
+
mode: "single" as const,
|
|
30
|
+
placeholder: "Ընտրել ամսաթիվ",
|
|
31
|
+
locale: "hy-AM" as const,
|
|
32
|
+
defaultViewDate: getCurrentMonthStart(),
|
|
33
|
+
defaultOpen: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const meta = {
|
|
37
|
+
title: "Components/DatePicker",
|
|
38
|
+
component: DatePicker,
|
|
39
|
+
parameters: {
|
|
40
|
+
layout: "centered",
|
|
41
|
+
},
|
|
42
|
+
args: sharedDatePickerArgs,
|
|
43
|
+
} satisfies Meta<DatePickerSingleProps>;
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
/** Default — closed field; click input to open calendar. */
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
render: function DefaultDatePickerStory() {
|
|
51
|
+
const [value, setValue] = useState<Date | undefined>();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View style={styles.field}>
|
|
55
|
+
<DatePicker {...sharedDatePickerArgs} value={value} onChange={setValue} />
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithSelectedDate: Story = {
|
|
62
|
+
render: function SelectedDatePickerStory() {
|
|
63
|
+
const [value, setValue] = useState<Date | undefined>(figmaSampleDate);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View style={styles.field}>
|
|
67
|
+
<DatePicker
|
|
68
|
+
{...sharedDatePickerArgs}
|
|
69
|
+
labels={figmaLabels}
|
|
70
|
+
defaultViewDate={figmaViewDate}
|
|
71
|
+
value={value}
|
|
72
|
+
onChange={setValue}
|
|
73
|
+
/>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Open: Story = {
|
|
80
|
+
render: function OpenDatePickerStory() {
|
|
81
|
+
const [value, setValue] = useState<Date | undefined>();
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<View style={styles.canvas}>
|
|
85
|
+
<DatePicker {...sharedDatePickerArgs} defaultOpen value={value} onChange={setValue} />
|
|
86
|
+
</View>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const WithLabel: Story = {
|
|
92
|
+
render: function LabeledDatePickerStory() {
|
|
93
|
+
const [value, setValue] = useState<Date | undefined>();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<View style={styles.field}>
|
|
97
|
+
<DatePicker {...sharedDatePickerArgs} label="Ամսաթիվ" value={value} onChange={setValue} />
|
|
98
|
+
</View>
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const WithError: Story = {
|
|
104
|
+
render: function ErrorDatePickerStory() {
|
|
105
|
+
const [value, setValue] = useState<Date | undefined>();
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<View style={styles.field}>
|
|
109
|
+
<DatePicker
|
|
110
|
+
{...sharedDatePickerArgs}
|
|
111
|
+
error="Խնդրում ենք ընտրել ամսաթիվ"
|
|
112
|
+
invalid
|
|
113
|
+
value={value}
|
|
114
|
+
onChange={setValue}
|
|
115
|
+
/>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const Disabled: Story = {
|
|
122
|
+
render: function DisabledDatePickerStory() {
|
|
123
|
+
return (
|
|
124
|
+
<View style={styles.field}>
|
|
125
|
+
<DatePicker {...sharedDatePickerArgs} disabled value={figmaSampleDate} />
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const Range = {
|
|
132
|
+
render: function RangeDatePickerStory() {
|
|
133
|
+
const [value, setValue] = useState<DateRange | undefined>(getSampleRange);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<View style={styles.field}>
|
|
137
|
+
<DatePicker
|
|
138
|
+
mode="range"
|
|
139
|
+
placeholder="Ընտրել ժամանակահատված"
|
|
140
|
+
locale="hy-AM"
|
|
141
|
+
defaultViewDate={getCurrentMonthStart()}
|
|
142
|
+
value={value}
|
|
143
|
+
onChange={setValue}
|
|
144
|
+
/>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
} satisfies StoryObj<DatePickerRangeProps>;
|
|
149
|
+
|
|
150
|
+
export const RangeOpen = {
|
|
151
|
+
render: function RangeOpenDatePickerStory() {
|
|
152
|
+
const [value, setValue] = useState<DateRange | undefined>(getSampleRange);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<View style={styles.canvas}>
|
|
156
|
+
<DatePicker
|
|
157
|
+
mode="range"
|
|
158
|
+
locale="en-US"
|
|
159
|
+
defaultViewDate={getCurrentMonthStart()}
|
|
160
|
+
defaultOpen
|
|
161
|
+
closeOnRangeComplete={false}
|
|
162
|
+
value={value}
|
|
163
|
+
onChange={setValue}
|
|
164
|
+
/>
|
|
165
|
+
</View>
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
} satisfies StoryObj<DatePickerRangeProps>;
|
|
169
|
+
|
|
170
|
+
export const RussianLocale: Story = {
|
|
171
|
+
render: function RussianDatePickerStory() {
|
|
172
|
+
const [value, setValue] = useState<Date | undefined>();
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<View style={styles.field}>
|
|
176
|
+
<DatePicker
|
|
177
|
+
mode="single"
|
|
178
|
+
locale="ru-RU"
|
|
179
|
+
defaultViewDate={getCurrentMonthStart()}
|
|
180
|
+
value={value}
|
|
181
|
+
onChange={setValue}
|
|
182
|
+
/>
|
|
183
|
+
</View>
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const RangeInteractive = {
|
|
189
|
+
render: function RangeInteractiveDatePickerStory() {
|
|
190
|
+
const [value, setValue] = useState<DateRange | undefined>(getSampleRange);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<View style={styles.canvas}>
|
|
194
|
+
<DatePicker
|
|
195
|
+
mode="range"
|
|
196
|
+
placeholder="Ընտրել ժամանակահատված"
|
|
197
|
+
locale="hy-AM"
|
|
198
|
+
defaultViewDate={getCurrentMonthStart()}
|
|
199
|
+
defaultOpen
|
|
200
|
+
closeOnRangeComplete={false}
|
|
201
|
+
value={value}
|
|
202
|
+
onChange={setValue}
|
|
203
|
+
/>
|
|
204
|
+
</View>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
} satisfies StoryObj<DatePickerRangeProps>;
|
|
208
|
+
|
|
209
|
+
const styles = StyleSheet.create({
|
|
210
|
+
field: {
|
|
211
|
+
width: 296,
|
|
212
|
+
maxWidth: "100%",
|
|
213
|
+
},
|
|
214
|
+
canvas: {
|
|
215
|
+
width: 296,
|
|
216
|
+
maxWidth: "100%",
|
|
217
|
+
minHeight: 520,
|
|
218
|
+
},
|
|
219
|
+
});
|