@swan-io/lake 2.3.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +5 -1
  2. package/src/assets/3d-card/environment/nx.png +0 -0
  3. package/src/assets/3d-card/environment/ny.png +0 -0
  4. package/src/assets/3d-card/environment/nz.png +0 -0
  5. package/src/assets/3d-card/environment/px.png +0 -0
  6. package/src/assets/3d-card/environment/py.png +0 -0
  7. package/src/assets/3d-card/environment/pz.png +0 -0
  8. package/src/assets/3d-card/model/MaisonNeue-Book.woff +0 -0
  9. package/src/assets/3d-card/model/MarkPro-Regular.ttf +0 -0
  10. package/src/assets/3d-card/model/band_roughness.jpg +0 -0
  11. package/src/assets/3d-card/model/card.gltf +1094 -0
  12. package/src/assets/3d-card/model/chip.jpg +0 -0
  13. package/src/assets/3d-card/model/color_black.jpg +0 -0
  14. package/src/assets/3d-card/model/color_silver.jpg +0 -0
  15. package/src/assets/3d-card/shaders/shinyColorFragment.glsl +11 -0
  16. package/src/components/AutoWidthImage.d.ts +1 -1
  17. package/src/components/Card3dPreview.d.ts +19 -0
  18. package/src/components/Card3dPreview.js +168 -0
  19. package/src/components/DatePicker.d.ts +90 -0
  20. package/src/components/DatePicker.js +619 -0
  21. package/src/components/Filters.d.ts +10 -7
  22. package/src/components/Filters.js +22 -31
  23. package/src/components/FixedListViewCells.d.ts +4 -3
  24. package/src/components/FixedListViewCells.js +2 -2
  25. package/src/components/Icon.d.ts +4 -0
  26. package/src/components/LakeCombobox.d.ts +6 -1
  27. package/src/components/LakeCombobox.js +7 -8
  28. package/src/components/LakeText.d.ts +1 -1
  29. package/src/components/LakeTextInput.d.ts +1 -1
  30. package/src/components/PlainListView.d.ts +2 -1
  31. package/src/components/PlainListView.js +2 -2
  32. package/src/components/QuickActions.d.ts +4 -3
  33. package/src/components/QuickActions.js +2 -2
  34. package/src/components/Stack.d.ts +1 -1
  35. package/src/components/TimePicker.d.ts +55 -0
  36. package/src/components/TimePicker.js +170 -0
  37. package/src/icons/custom-icons.json +2 -0
  38. package/src/icons/fluent-icons.json +2 -0
  39. package/src/utils/svg.d.ts +10 -0
  40. package/src/utils/svg.js +147 -0
@@ -0,0 +1,619 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Option } from "@swan-io/boxed";
3
+ import dayjs from "dayjs";
4
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
5
+ import { StyleSheet, View } from "react-native";
6
+ import { useForm } from "react-ux-form";
7
+ import { Rifm } from "rifm";
8
+ import { P, match } from "ts-pattern";
9
+ import { colors, spacings } from "../constants/design";
10
+ import { useDisclosure } from "../hooks/useDisclosure";
11
+ import { useFirstMountState } from "../hooks/useFirstMountState";
12
+ import { useResponsive } from "../hooks/useResponsive";
13
+ import { noop } from "../utils/function";
14
+ import { isNotNullish, isNotNullishOrEmpty, isNullishOrEmpty } from "../utils/nullish";
15
+ import { getRifmProps } from "../utils/rifm";
16
+ import { BottomPanel } from "./BottomPanel";
17
+ import { Box } from "./Box";
18
+ import { Fill } from "./Fill";
19
+ import { Icon } from "./Icon";
20
+ import { LakeButton } from "./LakeButton";
21
+ import { LakeLabel } from "./LakeLabel";
22
+ import { LakeModal } from "./LakeModal";
23
+ import { LakeSelect } from "./LakeSelect";
24
+ import { LakeText } from "./LakeText";
25
+ import { LakeTextInput } from "./LakeTextInput";
26
+ import { Popover } from "./Popover";
27
+ import { Pressable } from "./Pressable";
28
+ import { Separator } from "./Separator";
29
+ import { Space } from "./Space";
30
+ const styles = StyleSheet.create({
31
+ label: {
32
+ flex: 1,
33
+ },
34
+ arrowContainer: {
35
+ height: 40, // input height
36
+ },
37
+ popover: {
38
+ padding: spacings[12],
39
+ },
40
+ popoverDesktop: {
41
+ padding: spacings[24],
42
+ },
43
+ rangeCalendarSide: {
44
+ flex: 1,
45
+ },
46
+ button: {
47
+ flex: 1,
48
+ },
49
+ monthSelect: {
50
+ width: 130,
51
+ },
52
+ yearSelect: {
53
+ width: 100,
54
+ },
55
+ weekRow: {
56
+ paddingVertical: spacings[4],
57
+ },
58
+ dayName: {
59
+ flex: 1,
60
+ height: 32,
61
+ alignItems: "center",
62
+ justifyContent: "center",
63
+ },
64
+ dayContainer: {
65
+ flex: 1,
66
+ alignItems: "center",
67
+ },
68
+ dayRangeIndicator: {
69
+ position: "absolute",
70
+ top: 0,
71
+ right: 0,
72
+ bottom: 0,
73
+ left: 0,
74
+ backgroundColor: colors.current[100],
75
+ },
76
+ dayStartRangeIndicator: {
77
+ left: "50%",
78
+ },
79
+ dayEndRangeIndicator: {
80
+ right: "50%",
81
+ },
82
+ dayNumber: {
83
+ width: 32,
84
+ height: 32,
85
+ alignItems: "center",
86
+ justifyContent: "center",
87
+ borderRadius: 16,
88
+ },
89
+ dayNumberFocused: {},
90
+ dayNumberHover: {
91
+ backgroundColor: colors.current[100],
92
+ },
93
+ dayNumberPressed: {},
94
+ dayNumberSelected: {
95
+ backgroundColor: colors.current[500],
96
+ },
97
+ todayIndicator: {
98
+ position: "absolute",
99
+ left: 0,
100
+ right: 0,
101
+ bottom: 0,
102
+ width: 4,
103
+ height: 4,
104
+ marginHorizontal: "auto",
105
+ borderRadius: 2,
106
+ backgroundColor: colors.current[500],
107
+ },
108
+ });
109
+ const MODALE_MOBILE_THRESHOLD = 600;
110
+ const DATE_PICKER_MOBILE_THRESHOLD = 400;
111
+ const DATE_RANGE_PICKER_THRESHOLD = 800;
112
+ const NB_DAYS_IN_WEEK = 7;
113
+ const weekDayIndex = {
114
+ sunday: 0,
115
+ monday: 1,
116
+ tuesday: 2,
117
+ wednesday: 3,
118
+ thursday: 4,
119
+ friday: 5,
120
+ saturday: 6,
121
+ };
122
+ const rifmDateProps = getRifmProps({
123
+ accept: "numeric",
124
+ charMap: { 2: "/", 4: "/" },
125
+ maxLength: 8,
126
+ });
127
+ const parseDate = (value, format) => {
128
+ const date = dayjs.utc(value, format);
129
+ return date.isValid()
130
+ ? Option.Some({ day: date.date(), month: date.month(), year: date.year() })
131
+ : Option.None();
132
+ };
133
+ const parseRange = (value, format) => {
134
+ return {
135
+ start: parseDate(value.start, format),
136
+ end: parseDate(value.end, format),
137
+ };
138
+ };
139
+ const stringifyDate = (value, format) => {
140
+ const date = dayjs.utc().year(value.year).month(value.month).date(value.day);
141
+ return date.format(format);
142
+ };
143
+ export const validateDateRangeOrder = (value, format) => {
144
+ const range = parseRange(value, format);
145
+ if (range.start.isNone() || range.end.isNone()) {
146
+ return true;
147
+ }
148
+ if (isDateAfter(range.start.value, range.end.value)) {
149
+ return false;
150
+ }
151
+ return true;
152
+ };
153
+ const range = (start, end) => {
154
+ const result = [];
155
+ for (let i = start; i <= end; i++) {
156
+ result.push(i);
157
+ }
158
+ return result;
159
+ };
160
+ const groupEvery = (input, groupSize) => {
161
+ const result = [];
162
+ const nbGroups = Math.ceil(input.length / groupSize);
163
+ for (let i = 0; i < nbGroups; i++) {
164
+ result.push(input.slice(i * groupSize, (i + 1) * groupSize));
165
+ }
166
+ return result;
167
+ };
168
+ const padEnd = (input, length, value) => {
169
+ const itemsToAppend = new Array(length - input.length).fill(value);
170
+ return [...input, ...itemsToAppend];
171
+ };
172
+ export const isTodayOrFutureDate = (date) => {
173
+ const yesterday = new Date();
174
+ yesterday.setDate(yesterday.getDate() - 1);
175
+ const yesterdayDate = {
176
+ day: yesterday.getDate(),
177
+ month: yesterday.getMonth(),
178
+ year: yesterday.getFullYear(),
179
+ };
180
+ return isDateAfter(date, yesterdayDate);
181
+ };
182
+ export const isDateInRange = (minDate, maxDate) => (date) => {
183
+ const min = {
184
+ day: minDate.getDate(),
185
+ month: minDate.getMonth(),
186
+ year: minDate.getFullYear(),
187
+ };
188
+ const max = {
189
+ day: maxDate.getDate(),
190
+ month: maxDate.getMonth(),
191
+ year: maxDate.getFullYear(),
192
+ };
193
+ return isDateAfter(date, min) && isDateBefore(date, max);
194
+ };
195
+ const isDateToday = (date) => {
196
+ const today = new Date();
197
+ return (date.day === today.getDate() &&
198
+ date.month === today.getMonth() &&
199
+ date.year === today.getFullYear());
200
+ };
201
+ const getMonthDates = (month, year) => {
202
+ const aggregate = (acc, date) => {
203
+ const dateDay = date.getDate();
204
+ const dateMonth = date.getMonth();
205
+ const dateYear = date.getFullYear();
206
+ if (date.getMonth() !== month) {
207
+ return acc;
208
+ }
209
+ return aggregate([...acc, { day: dateDay, month: dateMonth, year: dateYear }], new Date(year, month, dateDay + 1));
210
+ };
211
+ return aggregate([], new Date(year, month, 1));
212
+ };
213
+ const getMonthWeeks = (month, year, firstWeekDay) => {
214
+ const firstWeekDayIndex = weekDayIndex[firstWeekDay];
215
+ const monthFirstWeekDay = new Date(year, month, 1).getDay();
216
+ const monthDates = getMonthDates(month, year).map(date => Option.Some(date));
217
+ const nbDaysToPrepend = monthFirstWeekDay >= firstWeekDayIndex
218
+ ? monthFirstWeekDay - firstWeekDayIndex
219
+ : NB_DAYS_IN_WEEK - firstWeekDayIndex + monthFirstWeekDay;
220
+ for (let i = 0; i < nbDaysToPrepend; i++) {
221
+ monthDates.unshift(Option.None());
222
+ }
223
+ const weeks = groupEvery(monthDates, NB_DAYS_IN_WEEK);
224
+ const lastWeek = weeks[weeks.length - 1];
225
+ if (!lastWeek) {
226
+ return weeks;
227
+ }
228
+ weeks[weeks.length - 1] = padEnd(lastWeek, NB_DAYS_IN_WEEK, Option.None());
229
+ return weeks;
230
+ };
231
+ const getWeekDayNames = (dayNames, firstWeekDay = "sunday") => {
232
+ const firstWeekDayIndex = weekDayIndex[firstWeekDay];
233
+ const firstWeekDayNames = dayNames.slice(firstWeekDayIndex);
234
+ const lastWeekDayNames = dayNames.slice(0, firstWeekDayIndex);
235
+ // @ts-expect-error
236
+ return [...firstWeekDayNames, ...lastWeekDayNames];
237
+ };
238
+ const isDateEquals = (date1, date2) => {
239
+ return date1.day === date2.day && date1.month === date2.month && date1.year === date2.year;
240
+ };
241
+ const isDateBefore = (date1, date2) => {
242
+ return (date1.year < date2.year ||
243
+ (date1.year === date2.year && date1.month < date2.month) ||
244
+ (date1.year === date2.year && date1.month === date2.month && date1.day < date2.day));
245
+ };
246
+ const isDateAfter = (date1, date2) => {
247
+ return (date1.year > date2.year ||
248
+ (date1.year === date2.year && date1.month > date2.month) ||
249
+ (date1.year === date2.year && date1.month === date2.month && date1.day > date2.day));
250
+ };
251
+ const isDateRange = (value) => {
252
+ return match(value)
253
+ .with({ start: P._, end: P._ }, () => true)
254
+ .otherwise(() => false);
255
+ };
256
+ const isSelectedDate = (date, value) => {
257
+ return match(value)
258
+ .with(Option.pattern.Some(P.select()), value => isDateEquals(value, date))
259
+ .with(Option.pattern.None, () => false)
260
+ .with(P.when(isDateRange), ({ start, end }) => {
261
+ // if range is invalid, we don't display any selection
262
+ if (start.isSome() && end.isSome() && isDateAfter(start.value, end.value)) {
263
+ return false;
264
+ }
265
+ return (start.match({
266
+ Some: start => isDateEquals(start, date),
267
+ None: () => false,
268
+ }) ||
269
+ end.match({
270
+ Some: end => isDateEquals(end, date),
271
+ None: () => false,
272
+ }));
273
+ })
274
+ .exhaustive();
275
+ };
276
+ const getRangeIndicatorType = (date, value) => {
277
+ if (!isDateRange(value)) {
278
+ return "none";
279
+ }
280
+ const { start, end } = value;
281
+ if (start.isNone() || end.isNone()) {
282
+ return "none";
283
+ }
284
+ const startDate = start.value;
285
+ const endDate = end.value;
286
+ // no interval indicator if range is invalid
287
+ if (isDateAfter(startDate, endDate)) {
288
+ return "none";
289
+ }
290
+ if (isDateEquals(startDate, endDate)) {
291
+ return "none";
292
+ }
293
+ if (isDateEquals(date, startDate)) {
294
+ return "start";
295
+ }
296
+ if (isDateEquals(date, endDate)) {
297
+ return "end";
298
+ }
299
+ if (isDateAfter(date, startDate) && isDateBefore(date, endDate)) {
300
+ return "between";
301
+ }
302
+ return "none";
303
+ };
304
+ const computeDateDistanceInDays = (date1, date2) => {
305
+ const date1Date = new Date(date1.year, date1.month, date1.day);
306
+ const date2Date = new Date(date2.year, date2.month, date2.day);
307
+ const diffInMs = Math.abs(date2Date.getTime() - date1Date.getTime());
308
+ return Math.round(diffInMs / (1000 * 3600 * 24));
309
+ };
310
+ const getNewDateRange = (currentRange, selectedDate) => {
311
+ const { start, end } = currentRange;
312
+ // Handle initial selection
313
+ if (start.isNone()) {
314
+ return { start: Option.Some(selectedDate), end: Option.None() };
315
+ }
316
+ if (end.isNone()) {
317
+ if (isDateAfter(selectedDate, start.value)) {
318
+ return { start, end: Option.Some(selectedDate) };
319
+ }
320
+ return { start: Option.Some(selectedDate), end: currentRange.start };
321
+ }
322
+ // Handle selection outside of the current range
323
+ if (isDateBefore(selectedDate, start.value)) {
324
+ return { start: Option.Some(selectedDate), end: currentRange.end };
325
+ }
326
+ if (isDateAfter(selectedDate, end.value)) {
327
+ return { start: currentRange.start, end: Option.Some(selectedDate) };
328
+ }
329
+ // We change the closest date to the new date
330
+ const startDistance = computeDateDistanceInDays(start.value, selectedDate);
331
+ const endDistance = computeDateDistanceInDays(end.value, selectedDate);
332
+ if (startDistance < endDistance) {
333
+ return { start: Option.Some(selectedDate), end: currentRange.end };
334
+ }
335
+ return { start: currentRange.start, end: Option.Some(selectedDate) };
336
+ };
337
+ const getTodayYearMonth = () => ({
338
+ month: new Date().getMonth(),
339
+ year: new Date().getFullYear(),
340
+ });
341
+ const getYearMonth = (value, format) => {
342
+ if (isNullishOrEmpty(value)) {
343
+ return Option.None();
344
+ }
345
+ return parseDate(value, format);
346
+ };
347
+ const isYearMonthBefore = (date1, date2) => {
348
+ return date1.year < date2.year || (date1.year === date2.year && date1.month < date2.month);
349
+ };
350
+ const isYearMonthEquals = (date1, date2) => {
351
+ return date1.year === date2.year && date1.month === date2.month;
352
+ };
353
+ const minYearMonth = (date1, date2) => {
354
+ return isYearMonthBefore(date1, date2) ? date1 : date2;
355
+ };
356
+ const maxYearMonth = (date1, date2) => {
357
+ return isYearMonthBefore(date1, date2) ? date2 : date1;
358
+ };
359
+ const incrementYearMonth = ({ month, year }) => {
360
+ if (month === 11) {
361
+ return { month: 0, year: year + 1 };
362
+ }
363
+ return { month: month + 1, year };
364
+ };
365
+ const decrementYearMonth = ({ month, year }) => {
366
+ if (month === 0) {
367
+ return { month: 11, year: year - 1 };
368
+ }
369
+ return { month: month - 1, year };
370
+ };
371
+ const YearMonthSelect = ({ monthNames, value, arrowsPosition = "right", hideArrows, minValue, maxValue, onChange, }) => {
372
+ const monthItems = useMemo(() => monthNames.map((name, index) => ({ name, value: index })), [monthNames]);
373
+ const yearItems = useMemo(() => range(value.year - 5, value.year + 5).map(year => ({
374
+ name: year.toString(),
375
+ value: year,
376
+ })), [value.year]);
377
+ const selectMonth = (month) => {
378
+ onChange({ year: value.year, month });
379
+ };
380
+ const selectYear = (year) => {
381
+ onChange({ year, month: value.month });
382
+ };
383
+ const setPreviousMonth = () => {
384
+ onChange(decrementYearMonth(value));
385
+ };
386
+ const setNextMonth = () => {
387
+ onChange(incrementYearMonth(value));
388
+ };
389
+ const isPreviousDisabled = !minValue
390
+ ? false
391
+ : value.year <= minValue.year && value.month <= minValue.month;
392
+ const isNextDisabled = !maxValue
393
+ ? false
394
+ : value.year >= maxValue.year && value.month >= maxValue.month;
395
+ return (_jsxs(Box, { direction: "row", alignItems: "center", children: [arrowsPosition === "around" && hideArrows !== true && (_jsxs(_Fragment, { children: [_jsx(LakeButton, { size: "small", mode: "tertiary", icon: "arrow-left-filled", disabled: isPreviousDisabled, onPress: setPreviousMonth }), _jsx(Fill, { minWidth: 12 })] })), _jsx(LakeSelect, { items: monthItems, value: value.month, onValueChange: selectMonth, mode: "borderless", size: "small", hideErrors: true, style: styles.monthSelect }), _jsx(LakeSelect, { items: yearItems, value: value.year, onValueChange: selectYear, mode: "borderless", size: "small", hideErrors: true, style: styles.yearSelect }), hideArrows !== true && (_jsxs(_Fragment, { children: [_jsx(Fill, { minWidth: 12 }), arrowsPosition === "right" && (_jsxs(_Fragment, { children: [_jsx(LakeButton, { size: "small", mode: "tertiary", icon: "arrow-left-filled", disabled: isPreviousDisabled, onPress: setPreviousMonth }), _jsx(Space, { width: 12 })] })), _jsx(LakeButton, { size: "small", mode: "tertiary", icon: "arrow-right-filled", disabled: isNextDisabled, onPress: setNextMonth })] }))] }));
396
+ };
397
+ const MonthCalendar = ({ month, year, value, firstWeekDay, weekDayNames, isSelectable, onChange, }) => {
398
+ const dayNames = useMemo(() => getWeekDayNames(weekDayNames, firstWeekDay), [weekDayNames, firstWeekDay]);
399
+ const weeks = useMemo(() => getMonthWeeks(month, year, firstWeekDay), [month, year, firstWeekDay]);
400
+ return (_jsxs(View, { children: [_jsx(Box, { direction: "row", alignItems: "center", style: styles.weekRow, children: dayNames.map(dayName => (_jsx(View, { style: styles.dayName, children: _jsx(LakeText, { variant: "medium", color: colors.gray[600], children: dayName.substring(0, 1) }) }, dayName))) }), weeks.map((week, weekIndex) => (_jsx(Box, { direction: "row", alignItems: "center", style: styles.weekRow, children: week.map((date, dateIndex) => {
401
+ const isDisabled = date.match({
402
+ Some: date => isNotNullish(isSelectable) && !isSelectable(date),
403
+ None: () => true,
404
+ });
405
+ const isSelected = date.match({
406
+ Some: date => isSelectedDate(date, value),
407
+ None: () => false,
408
+ });
409
+ const isToday = date.match({
410
+ Some: date => isDateToday(date),
411
+ None: () => false,
412
+ });
413
+ const rangeIndicator = date.match({
414
+ Some: date => getRangeIndicatorType(date, value),
415
+ None: () => "none",
416
+ });
417
+ return (_jsxs(View, { style: styles.dayContainer, children: [rangeIndicator !== "none" && (_jsx(View, { style: [
418
+ styles.dayRangeIndicator,
419
+ rangeIndicator === "start" && styles.dayStartRangeIndicator,
420
+ rangeIndicator === "end" && styles.dayEndRangeIndicator,
421
+ ] })), _jsxs(Pressable, { disabled: isDisabled, onPress: () => date.match({ Some: onChange, None: noop }), style: ({ focused, hovered, pressed }) => [
422
+ styles.dayNumber,
423
+ focused && styles.dayNumberFocused,
424
+ hovered && styles.dayNumberHover,
425
+ pressed && styles.dayNumberPressed,
426
+ isSelected && styles.dayNumberSelected,
427
+ ], children: [_jsx(LakeText, { variant: "smallRegular", color: isSelected
428
+ ? colors.current.contrast
429
+ : isDisabled
430
+ ? colors.gray[300]
431
+ : isToday
432
+ ? colors.current[500]
433
+ : colors.gray[900], children: date.match({ Some: ({ day }) => day.toString(), None: () => " " }) }), isToday && _jsx(View, { style: styles.todayIndicator })] })] }, dateIndex));
434
+ }) }, weekIndex)))] }));
435
+ };
436
+ const DatePickerPopoverContent = ({ value, format, firstWeekDay, monthNames, weekDayNames, desktop, isSelectable, onChange, }) => {
437
+ const [monthYear, setMonthYear] = useState(() => getYearMonth(value, format).getWithDefault(getTodayYearMonth()));
438
+ // Automatically change displayed year and month when user change the value with text input
439
+ useEffect(() => {
440
+ const yearMonth = getYearMonth(value, format);
441
+ if (yearMonth.isSome()) {
442
+ setMonthYear(yearMonth.value);
443
+ }
444
+ }, [value, format]);
445
+ const handleChange = useCallback((date) => {
446
+ const formatted = stringifyDate(date, format);
447
+ onChange(formatted);
448
+ }, [format, onChange]);
449
+ return (_jsxs(_Fragment, { children: [_jsx(YearMonthSelect, { monthNames: monthNames, value: monthYear, hideArrows: !desktop, onChange: setMonthYear }), _jsx(Space, { height: 24 }), _jsx(MonthCalendar, { month: monthYear.month, year: monthYear.year, value: isNotNullishOrEmpty(value) ? parseDate(value, format) : Option.None(), firstWeekDay: firstWeekDay, weekDayNames: weekDayNames, isSelectable: isSelectable, onChange: handleChange })] }));
450
+ };
451
+ export const DatePicker = ({ label, value, error, format, firstWeekDay, monthNames, weekDayNames, isSelectable, onChange, }) => {
452
+ const { desktop } = useResponsive(DATE_PICKER_MOBILE_THRESHOLD);
453
+ const ref = useRef(null);
454
+ const [isOpened, { open, close }] = useDisclosure(false);
455
+ const popoverId = useId();
456
+ return (_jsxs(_Fragment, { children: [_jsx(Box, { direction: "row", alignItems: "end", children: _jsx(LakeLabel, { label: label, style: styles.label, actions: _jsx(LakeButton, { mode: "secondary", icon: "calendar-ltr-regular", size: "small", onPress: open }), render: id => (_jsx(Rifm, { value: value ?? "", onChange: onChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { ref: ref, id: id, placeholder: format, value: value, error: error, onChange: onChange, ariaExpanded: isOpened })) })) }) }), _jsx(Popover, { id: popoverId, role: "dialog", onDismiss: close, referenceRef: ref, visible: isOpened, children: _jsx(View, { style: desktop ? styles.popoverDesktop : styles.popover, children: _jsx(DatePickerPopoverContent, { value: value, format: format, firstWeekDay: firstWeekDay, monthNames: monthNames, weekDayNames: weekDayNames, desktop: desktop, isSelectable: isSelectable, onChange: onChange }) }) })] }));
457
+ };
458
+ export const DatePickerModal = ({ value, format, firstWeekDay, monthNames, weekDayNames, isSelectable, onChange, visible, label, cancelLabel, confirmLabel, validate, onDissmiss, }) => {
459
+ const { desktop } = useResponsive(DATE_PICKER_MOBILE_THRESHOLD);
460
+ const { Field, submitForm, setFieldValue, resetField } = useForm({
461
+ date: {
462
+ initialValue: value ?? "",
463
+ validate,
464
+ },
465
+ });
466
+ const handleCancel = () => {
467
+ setFieldValue("date", value ?? "");
468
+ onDissmiss();
469
+ };
470
+ const handleConfirm = () => {
471
+ submitForm(({ date }) => {
472
+ if (isNotNullishOrEmpty(date)) {
473
+ onChange(date);
474
+ }
475
+ onDissmiss();
476
+ });
477
+ };
478
+ useEffect(() => {
479
+ if (!visible) {
480
+ resetField("date");
481
+ }
482
+ }, [visible, resetField]);
483
+ return (_jsxs(DateModal, { visible: visible, maxWidth: 500, onPressClose: handleCancel, children: [_jsx(Field, { name: "date", children: ({ ref, value, error, onBlur, onChange }) => (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: label, render: id => (_jsx(Rifm, { value: value, onChange: onChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { ref: ref, id: id, placeholder: format, value: value, error: error, onBlur: onBlur, onChange: onChange })) })) }), _jsx(DatePickerPopoverContent, { value: value, format: format, firstWeekDay: firstWeekDay, monthNames: monthNames, weekDayNames: weekDayNames, desktop: desktop, isSelectable: isSelectable, onChange: onChange })] })) }), _jsx(Space, { height: 24 }), _jsxs(Box, { direction: "row", alignItems: "center", children: [_jsx(LakeButton, { mode: "secondary", size: "small", onPress: handleCancel, style: styles.button, children: cancelLabel }), _jsx(Space, { width: 24 }), _jsx(LakeButton, { color: "current", size: "small", onPress: handleConfirm, style: styles.button, children: confirmLabel })] })] }));
484
+ };
485
+ const DateModal = ({ children, visible, maxWidth, withCloseButton, onPressClose, }) => {
486
+ const { desktop } = useResponsive(MODALE_MOBILE_THRESHOLD);
487
+ if (desktop) {
488
+ return (_jsx(LakeModal, { visible: visible, maxWidth: maxWidth, onPressClose: withCloseButton === true ? onPressClose : undefined, children: children }));
489
+ }
490
+ return (_jsx(BottomPanel, { visible: visible, onPressClose: onPressClose, children: _jsx(View, { style: styles.popover, children: children }) }));
491
+ };
492
+ const DateRangePickerModalContent = ({ value, format, firstWeekDay, monthNames, weekDayNames, desktop, displayTwoCalendar, isSelectable, onChange, }) => {
493
+ const isFirstMount = useFirstMountState();
494
+ const [periods, setPeriods] = useState(() => {
495
+ const startYearMonth = getYearMonth(value.start, format).getWithDefault(getTodayYearMonth());
496
+ const endYearMonth = getYearMonth(value.end, format).getWithDefault(incrementYearMonth(startYearMonth));
497
+ return {
498
+ start: startYearMonth,
499
+ end: isYearMonthEquals(startYearMonth, endYearMonth)
500
+ ? incrementYearMonth(startYearMonth)
501
+ : endYearMonth,
502
+ };
503
+ });
504
+ // Automatically change displayed year and month when start date changes
505
+ useEffect(() => {
506
+ if (isFirstMount) {
507
+ return;
508
+ }
509
+ const startYearMonth = getYearMonth(value.start, format);
510
+ if (startYearMonth.isSome()) {
511
+ setPeriods(periods => {
512
+ const isStartAndEndSameMonth = isYearMonthEquals(startYearMonth.value, periods.end);
513
+ if (isStartAndEndSameMonth) {
514
+ return {
515
+ start: decrementYearMonth(periods.end),
516
+ end: periods.end,
517
+ };
518
+ }
519
+ // change end period if it becomes before start period
520
+ const endPeriod = maxYearMonth(periods.end, incrementYearMonth(startYearMonth.value));
521
+ return {
522
+ start: startYearMonth.value,
523
+ end: endPeriod,
524
+ };
525
+ });
526
+ }
527
+ }, [isFirstMount, value.start, format]);
528
+ // Automatically change displayed year and month when end date changes
529
+ useEffect(() => {
530
+ if (isFirstMount) {
531
+ return;
532
+ }
533
+ const endYearMonth = getYearMonth(value.end, format);
534
+ if (endYearMonth.isSome()) {
535
+ setPeriods(periods => {
536
+ const isStartAndEndSameMonth = isYearMonthEquals(periods.start, endYearMonth.value);
537
+ if (isStartAndEndSameMonth) {
538
+ return {
539
+ start: periods.start,
540
+ end: incrementYearMonth(periods.start),
541
+ };
542
+ }
543
+ // change start period if it becomes after end period
544
+ const startPeriod = minYearMonth(periods.start, decrementYearMonth(endYearMonth.value));
545
+ return {
546
+ start: startPeriod,
547
+ end: endYearMonth.value,
548
+ };
549
+ });
550
+ }
551
+ }, [isFirstMount, value.end, format]);
552
+ const setStartPeriod = useCallback((yearMonth) => {
553
+ setPeriods(periods => ({
554
+ start: yearMonth,
555
+ end: maxYearMonth(periods.end, incrementYearMonth(yearMonth)),
556
+ }));
557
+ }, []);
558
+ const setEndPeriod = useCallback((yearMonth) => {
559
+ setPeriods(periods => ({
560
+ start: minYearMonth(periods.start, decrementYearMonth(yearMonth)),
561
+ end: yearMonth,
562
+ }));
563
+ }, []);
564
+ const dateRange = useMemo(() => parseRange(value, format), [value, format]);
565
+ const handleSelectDate = (date) => {
566
+ const newRange = getNewDateRange(dateRange, date);
567
+ const newValue = {
568
+ start: newRange.start.match({
569
+ Some: date => stringifyDate(date, format),
570
+ None: () => value.start,
571
+ }),
572
+ end: newRange.end.match({
573
+ Some: date => stringifyDate(date, format),
574
+ None: () => value.end,
575
+ }),
576
+ };
577
+ onChange(newValue);
578
+ };
579
+ if (!displayTwoCalendar) {
580
+ return (_jsxs(_Fragment, { children: [_jsx(YearMonthSelect, { monthNames: monthNames, value: periods.start, hideArrows: !desktop, onChange: setStartPeriod }), _jsx(Space, { height: 24 }), _jsx(MonthCalendar, { month: periods.start.month, year: periods.start.year, value: dateRange, firstWeekDay: firstWeekDay, weekDayNames: weekDayNames, isSelectable: isSelectable, onChange: handleSelectDate })] }));
581
+ }
582
+ return (_jsx(View, { children: _jsxs(Box, { direction: "row", alignItems: "start", children: [_jsxs(View, { style: styles.rangeCalendarSide, children: [_jsx(YearMonthSelect, { monthNames: monthNames, value: periods.start, maxValue: decrementYearMonth(periods.end), arrowsPosition: "around", onChange: setStartPeriod }), _jsx(Space, { height: 24 }), _jsx(MonthCalendar, { month: periods.start.month, year: periods.start.year, value: dateRange, firstWeekDay: firstWeekDay, weekDayNames: weekDayNames, isSelectable: isSelectable, onChange: handleSelectDate })] }), _jsx(Separator, { space: 24, horizontal: true }), _jsxs(View, { style: styles.rangeCalendarSide, children: [_jsx(YearMonthSelect, { monthNames: monthNames, value: periods.end, minValue: incrementYearMonth(periods.start), arrowsPosition: "around", onChange: setEndPeriod }), _jsx(Space, { height: 24 }), _jsx(MonthCalendar, { month: periods.end.month, year: periods.end.year, value: dateRange, firstWeekDay: firstWeekDay, weekDayNames: weekDayNames, isSelectable: isSelectable, onChange: handleSelectDate })] })] }) }));
583
+ };
584
+ export const DateRangePicker = ({ value, error, format, startLabel, endLabel, firstWeekDay, monthNames, weekDayNames, isSelectable, onChange, }) => {
585
+ const { desktop } = useResponsive(DATE_PICKER_MOBILE_THRESHOLD);
586
+ const { desktop: displayTwoCalendar } = useResponsive(DATE_RANGE_PICKER_THRESHOLD);
587
+ const ref = useRef(null);
588
+ const [isOpened, { open, close }] = useDisclosure(false);
589
+ const handleStartChange = useCallback((start) => {
590
+ onChange({ start, end: value.end });
591
+ }, [value, onChange]);
592
+ const handleEndChange = useCallback((end) => {
593
+ onChange({ start: value.start, end });
594
+ }, [value, onChange]);
595
+ return (_jsxs(View, { children: [_jsxs(Box, { direction: "row", alignItems: "end", children: [_jsx(LakeLabel, { label: startLabel, style: styles.label, render: id => (_jsx(Rifm, { value: value.start, onChange: handleStartChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { ref: ref, id: id, placeholder: format, value: value, onChange: onChange, error: error, hideErrors: true, ariaExpanded: isOpened })) })) }), _jsx(Space, { width: 12 }), _jsx(Box, { style: styles.arrowContainer, justifyContent: "center", children: _jsx(Icon, { name: "arrow-right-filled", size: 20 }) }), _jsx(Space, { width: 12 }), _jsx(LakeLabel, { label: endLabel, style: styles.label, render: id => (_jsx(Rifm, { value: value.end, onChange: handleEndChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { id: id, placeholder: format, value: value, onChange: onChange, error: error, hideErrors: true, ariaExpanded: isOpened })) })) }), _jsx(Space, { width: 12 }), _jsx(LakeButton, { mode: "secondary", icon: "calendar-ltr-regular", size: "small", onPress: open })] }), _jsx(Space, { height: 4 }), _jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], children: error ?? " " }), _jsx(DateModal, { visible: isOpened, maxWidth: 900, withCloseButton: true, onPressClose: close, children: _jsx(DateRangePickerModalContent, { value: value, format: format, firstWeekDay: firstWeekDay, monthNames: monthNames, weekDayNames: weekDayNames, desktop: desktop, displayTwoCalendar: displayTwoCalendar, isSelectable: isSelectable, onChange: onChange }) })] }));
596
+ };
597
+ export const DateRangePickerModal = ({ value, error, format, firstWeekDay, monthNames, weekDayNames, isSelectable, onChange, visible, startLabel, endLabel, cancelLabel, confirmLabel, onDissmiss, }) => {
598
+ const { desktop } = useResponsive(MODALE_MOBILE_THRESHOLD);
599
+ const { desktop: displayTwoCalendar } = useResponsive(DATE_RANGE_PICKER_THRESHOLD);
600
+ const [localeValue, setLocaleValue] = useState(value);
601
+ useEffect(() => {
602
+ setLocaleValue(value);
603
+ }, [value]);
604
+ const handleStartChange = (start) => {
605
+ setLocaleValue({ start, end: localeValue.end });
606
+ };
607
+ const handleEndChange = (end) => {
608
+ setLocaleValue({ start: localeValue.start, end });
609
+ };
610
+ const handleCancel = () => {
611
+ setLocaleValue(value);
612
+ onDissmiss();
613
+ };
614
+ const handleConfirm = () => {
615
+ onChange(localeValue);
616
+ onDissmiss();
617
+ };
618
+ return (_jsxs(DateModal, { visible: visible, maxWidth: 900, onPressClose: handleCancel, children: [_jsxs(View, { children: [_jsxs(Box, { direction: "row", alignItems: "end", children: [_jsx(LakeLabel, { label: startLabel, style: styles.label, render: id => (_jsx(Rifm, { value: localeValue.start, onChange: handleStartChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { id: id, placeholder: format, value: value, onChange: onChange, error: error, hideErrors: true })) })) }), _jsx(Space, { width: 12 }), _jsx(Box, { style: styles.arrowContainer, justifyContent: "center", children: _jsx(Icon, { name: "arrow-right-filled", size: 20 }) }), _jsx(Space, { width: 12 }), _jsx(LakeLabel, { label: endLabel, style: styles.label, render: id => (_jsx(Rifm, { value: localeValue.end, onChange: handleEndChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { id: id, placeholder: format, value: value, onChange: onChange, error: error, hideErrors: true })) })) })] }), _jsx(Space, { height: 4 }), _jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], children: error ?? " " })] }), _jsx(DateRangePickerModalContent, { value: localeValue, format: format, firstWeekDay: firstWeekDay, monthNames: monthNames, weekDayNames: weekDayNames, desktop: desktop, displayTwoCalendar: displayTwoCalendar, isSelectable: isSelectable, onChange: setLocaleValue }), _jsx(Space, { height: 24 }), _jsxs(Box, { direction: "row", alignItems: "center", children: [_jsx(LakeButton, { mode: "secondary", size: "small", onPress: handleCancel, style: styles.button, children: cancelLabel }), _jsx(Space, { width: 24 }), _jsx(LakeButton, { color: "current", size: "small", onPress: handleConfirm, style: styles.button, children: confirmLabel })] })] }));
619
+ };
@@ -1,16 +1,15 @@
1
- import { ComponentProps } from "react";
2
1
  import { ValidatorResult } from "react-ux-form";
3
- import { Rifm } from "rifm";
4
2
  import { Simplify } from "type-fest";
3
+ import { DateFormat, DatePickerDate, MonthNames, WeekDayNames } from "./DatePicker";
5
4
  type Item<T> = {
6
5
  label: string;
7
6
  value: T;
8
7
  };
9
- type RifmProps = Required<Pick<ComponentProps<typeof Rifm>, "accept" | "append" | "format" | "mask">>;
10
8
  export type FilterCheckboxDef<T> = {
11
9
  type: "checkbox";
12
10
  label: string;
13
11
  items: Item<T>[];
12
+ width?: number;
14
13
  submitText: string;
15
14
  checkAllLabel?: string;
16
15
  };
@@ -18,15 +17,19 @@ export type FilterRadioDef<T> = {
18
17
  type: "radio";
19
18
  label: string;
20
19
  items: Item<T>[];
20
+ width?: number;
21
21
  };
22
- export type FilterDateDef = {
22
+ export type FilterDateDef<Values = unknown> = {
23
23
  type: "date";
24
24
  label: string;
25
+ monthNames: MonthNames;
26
+ dayNames: WeekDayNames;
27
+ cancelText: string;
25
28
  submitText: string;
26
29
  noValueText: string;
27
- dateFormat: string;
28
- rifmProps: RifmProps;
29
- validate?: (value: string) => ValidatorResult;
30
+ dateFormat: DateFormat;
31
+ isSelectable?: (date: DatePickerDate, filters: Values) => boolean;
32
+ validate?: (value: string, filters: Values) => ValidatorResult;
30
33
  };
31
34
  export type FilterInputDef = {
32
35
  type: "input";