orn-ui 0.1.1 → 0.2.1
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 +20 -2
- package/lib/commonjs/atoms/PressableScale.js +9 -10
- package/lib/commonjs/atoms/PressableScale.js.map +1 -1
- package/lib/commonjs/atoms/Skeleton.js +96 -0
- package/lib/commonjs/atoms/Skeleton.js.map +1 -0
- package/lib/commonjs/atoms/Transition.js +171 -0
- package/lib/commonjs/atoms/Transition.js.map +1 -0
- package/lib/commonjs/atoms/index.js +20 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/molecules/OptionCard.js +55 -22
- package/lib/commonjs/molecules/OptionCard.js.map +1 -1
- package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +36 -36
- package/lib/commonjs/molecules/SegmentedControl.js.map +1 -0
- package/lib/commonjs/molecules/Stepper.js +124 -47
- package/lib/commonjs/molecules/Stepper.js.map +1 -1
- package/lib/commonjs/molecules/Steps.js +36 -6
- package/lib/commonjs/molecules/Steps.js.map +1 -1
- package/lib/commonjs/molecules/index.js +9 -3
- package/lib/commonjs/molecules/index.js.map +1 -1
- package/lib/commonjs/organisms/AlertProvider.js +26 -0
- package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
- package/lib/commonjs/organisms/BottomSheet.js +118 -39
- package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
- package/lib/commonjs/organisms/DateField.js +46 -15
- package/lib/commonjs/organisms/DateField.js.map +1 -1
- package/lib/commonjs/organisms/DatePicker.js +129 -50
- package/lib/commonjs/organisms/DatePicker.js.map +1 -1
- package/lib/commonjs/organisms/List.js +66 -44
- package/lib/commonjs/organisms/List.js.map +1 -1
- package/lib/commonjs/organisms/Modal.js +42 -87
- package/lib/commonjs/organisms/Modal.js.map +1 -1
- package/lib/commonjs/organisms/NavigationBar.js +210 -0
- package/lib/commonjs/organisms/NavigationBar.js.map +1 -0
- package/lib/commonjs/organisms/Screen.js +9 -2
- package/lib/commonjs/organisms/Screen.js.map +1 -1
- package/lib/commonjs/organisms/SearchList.js +95 -27
- package/lib/commonjs/organisms/SearchList.js.map +1 -1
- package/lib/commonjs/organisms/Select.js +222 -50
- package/lib/commonjs/organisms/Select.js.map +1 -1
- package/lib/commonjs/organisms/ThemeToggle.js +52 -0
- package/lib/commonjs/organisms/ThemeToggle.js.map +1 -0
- package/lib/commonjs/organisms/Toast.js +8 -22
- package/lib/commonjs/organisms/Toast.js.map +1 -1
- package/lib/commonjs/organisms/ToastProvider.js +24 -0
- package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
- package/lib/commonjs/organisms/Wizard.js +23 -2
- package/lib/commonjs/organisms/Wizard.js.map +1 -1
- package/lib/commonjs/organisms/imperativeBridge.js +28 -0
- package/lib/commonjs/organisms/imperativeBridge.js.map +1 -0
- package/lib/commonjs/organisms/index.js +50 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/theme/colors.js +207 -0
- package/lib/commonjs/theme/colors.js.map +1 -0
- package/lib/commonjs/theme/createTheme.js +25 -2
- package/lib/commonjs/theme/createTheme.js.map +1 -1
- package/lib/commonjs/theme/index.js +25 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/labels.js +5 -1
- package/lib/commonjs/theme/labels.js.map +1 -1
- package/lib/commonjs/theme/palettes.js +5 -2
- package/lib/commonjs/theme/palettes.js.map +1 -1
- package/lib/module/atoms/PressableScale.js +9 -10
- package/lib/module/atoms/PressableScale.js.map +1 -1
- package/lib/module/atoms/Skeleton.js +91 -0
- package/lib/module/atoms/Skeleton.js.map +1 -0
- package/lib/module/atoms/Transition.js +165 -0
- package/lib/module/atoms/Transition.js.map +1 -0
- package/lib/module/atoms/index.js +2 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/molecules/OptionCard.js +56 -23
- package/lib/module/molecules/OptionCard.js.map +1 -1
- package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +36 -36
- package/lib/module/molecules/SegmentedControl.js.map +1 -0
- package/lib/module/molecules/Stepper.js +125 -49
- package/lib/module/molecules/Stepper.js.map +1 -1
- package/lib/module/molecules/Steps.js +37 -7
- package/lib/module/molecules/Steps.js.map +1 -1
- package/lib/module/molecules/index.js +2 -2
- package/lib/module/molecules/index.js.map +1 -1
- package/lib/module/organisms/AlertProvider.js +24 -1
- package/lib/module/organisms/AlertProvider.js.map +1 -1
- package/lib/module/organisms/BottomSheet.js +120 -41
- package/lib/module/organisms/BottomSheet.js.map +1 -1
- package/lib/module/organisms/DateField.js +49 -18
- package/lib/module/organisms/DateField.js.map +1 -1
- package/lib/module/organisms/DatePicker.js +131 -52
- package/lib/module/organisms/DatePicker.js.map +1 -1
- package/lib/module/organisms/List.js +68 -46
- package/lib/module/organisms/List.js.map +1 -1
- package/lib/module/organisms/Modal.js +44 -89
- package/lib/module/organisms/Modal.js.map +1 -1
- package/lib/module/organisms/NavigationBar.js +205 -0
- package/lib/module/organisms/NavigationBar.js.map +1 -0
- package/lib/module/organisms/Screen.js +9 -2
- package/lib/module/organisms/Screen.js.map +1 -1
- package/lib/module/organisms/SearchList.js +96 -28
- package/lib/module/organisms/SearchList.js.map +1 -1
- package/lib/module/organisms/Select.js +224 -52
- package/lib/module/organisms/Select.js.map +1 -1
- package/lib/module/organisms/ThemeToggle.js +47 -0
- package/lib/module/organisms/ThemeToggle.js.map +1 -0
- package/lib/module/organisms/Toast.js +10 -24
- package/lib/module/organisms/Toast.js.map +1 -1
- package/lib/module/organisms/ToastProvider.js +21 -0
- package/lib/module/organisms/ToastProvider.js.map +1 -1
- package/lib/module/organisms/Wizard.js +24 -3
- package/lib/module/organisms/Wizard.js.map +1 -1
- package/lib/module/organisms/imperativeBridge.js +24 -0
- package/lib/module/organisms/imperativeBridge.js.map +1 -0
- package/lib/module/organisms/index.js +4 -2
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/theme/colors.js +194 -0
- package/lib/module/theme/colors.js.map +1 -0
- package/lib/module/theme/createTheme.js +25 -2
- package/lib/module/theme/createTheme.js.map +1 -1
- package/lib/module/theme/index.js +1 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/labels.js +5 -1
- package/lib/module/theme/labels.js.map +1 -1
- package/lib/module/theme/palettes.js +5 -2
- package/lib/module/theme/palettes.js.map +1 -1
- package/lib/typescript/atoms/PressableScale.d.ts +3 -2
- package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
- package/lib/typescript/atoms/Skeleton.d.ts +29 -0
- package/lib/typescript/atoms/Skeleton.d.ts.map +1 -0
- package/lib/typescript/atoms/Transition.d.ts +51 -0
- package/lib/typescript/atoms/Transition.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +4 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/molecules/OptionCard.d.ts +11 -6
- package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
- package/lib/typescript/molecules/SegmentedControl.d.ts +24 -0
- package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/molecules/Stepper.d.ts +35 -3
- package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
- package/lib/typescript/molecules/Steps.d.ts.map +1 -1
- package/lib/typescript/molecules/index.d.ts +4 -4
- package/lib/typescript/molecules/index.d.ts.map +1 -1
- package/lib/typescript/organisms/AlertProvider.d.ts +9 -0
- package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/BottomSheet.d.ts +26 -4
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/organisms/DateField.d.ts +23 -5
- package/lib/typescript/organisms/DateField.d.ts.map +1 -1
- package/lib/typescript/organisms/DatePicker.d.ts +5 -1
- package/lib/typescript/organisms/DatePicker.d.ts.map +1 -1
- package/lib/typescript/organisms/List.d.ts +9 -2
- package/lib/typescript/organisms/List.d.ts.map +1 -1
- package/lib/typescript/organisms/Modal.d.ts +3 -5
- package/lib/typescript/organisms/Modal.d.ts.map +1 -1
- package/lib/typescript/organisms/NavigationBar.d.ts +32 -0
- package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -0
- package/lib/typescript/organisms/Screen.d.ts +2 -1
- package/lib/typescript/organisms/Screen.d.ts.map +1 -1
- package/lib/typescript/organisms/SearchList.d.ts +21 -5
- package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
- package/lib/typescript/organisms/Select.d.ts +19 -4
- package/lib/typescript/organisms/Select.d.ts.map +1 -1
- package/lib/typescript/{molecules → organisms}/ThemeToggle.d.ts +4 -4
- package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -0
- package/lib/typescript/organisms/Toast.d.ts.map +1 -1
- package/lib/typescript/organisms/ToastProvider.d.ts +8 -0
- package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/Wizard.d.ts +7 -1
- package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
- package/lib/typescript/organisms/imperativeBridge.d.ts +12 -0
- package/lib/typescript/organisms/imperativeBridge.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +7 -3
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/theme/colors.d.ts +44 -0
- package/lib/typescript/theme/colors.d.ts.map +1 -0
- package/lib/typescript/theme/createTheme.d.ts +3 -0
- package/lib/typescript/theme/createTheme.d.ts.map +1 -1
- package/lib/typescript/theme/index.d.ts +3 -1
- package/lib/typescript/theme/index.d.ts.map +1 -1
- package/lib/typescript/theme/labels.d.ts +6 -0
- package/lib/typescript/theme/labels.d.ts.map +1 -1
- package/lib/typescript/theme/palettes.d.ts.map +1 -1
- package/lib/typescript/theme/types.d.ts +9 -0
- package/lib/typescript/theme/types.d.ts.map +1 -1
- package/package.json +67 -6
- package/registry/alert-provider.json +9 -2
- package/registry/bottom-sheet.json +6 -2
- package/registry/core.json +9 -5
- package/registry/date-field.json +2 -1
- package/registry/date-picker.json +1 -1
- package/registry/index.json +99 -30
- package/registry/list.json +2 -1
- package/registry/manifest.json +44 -6
- package/registry/modal.json +3 -2
- package/registry/navigation-bar.json +18 -0
- package/registry/option-card.json +3 -2
- package/registry/pressable-scale.json +1 -1
- package/registry/screen.json +1 -1
- package/registry/search-list.json +2 -1
- package/registry/segmented-control.json +18 -0
- package/registry/select.json +1 -1
- package/registry/skeleton.json +18 -0
- package/registry/stepper.json +3 -2
- package/registry/steps.json +1 -1
- package/registry/theme-toggle.json +5 -4
- package/registry/toast-provider.json +11 -3
- package/registry/toast.json +3 -2
- package/registry/transition.json +19 -0
- package/registry/wizard.json +3 -2
- package/src/atoms/PressableScale.tsx +16 -6
- package/src/atoms/Skeleton.tsx +114 -0
- package/src/atoms/Transition.tsx +189 -0
- package/src/atoms/index.ts +4 -0
- package/src/molecules/OptionCard.tsx +62 -25
- package/src/molecules/SegmentedControl.tsx +84 -0
- package/src/molecules/Stepper.tsx +147 -32
- package/src/molecules/Steps.tsx +27 -8
- package/src/molecules/index.ts +4 -4
- package/src/organisms/AlertProvider.tsx +26 -1
- package/src/organisms/BottomSheet.tsx +214 -62
- package/src/organisms/DateField.tsx +79 -19
- package/src/organisms/DatePicker.tsx +198 -80
- package/src/organisms/List.tsx +73 -41
- package/src/organisms/Modal.tsx +49 -61
- package/src/organisms/NavigationBar.tsx +226 -0
- package/src/organisms/Screen.tsx +11 -3
- package/src/organisms/SearchList.tsx +131 -35
- package/src/organisms/Select.tsx +223 -45
- package/src/organisms/ThemeToggle.tsx +30 -0
- package/src/organisms/Toast.tsx +8 -21
- package/src/organisms/ToastProvider.tsx +24 -0
- package/src/organisms/Wizard.tsx +37 -3
- package/src/organisms/imperativeBridge.ts +28 -0
- package/src/organisms/index.ts +7 -3
- package/src/theme/colors.ts +175 -0
- package/src/theme/createTheme.ts +38 -4
- package/src/theme/index.ts +3 -1
- package/src/theme/labels.ts +13 -0
- package/src/theme/palettes.ts +5 -2
- package/src/theme/types.ts +10 -0
- package/lib/commonjs/molecules/ThemeToggle.js.map +0 -1
- package/lib/module/molecules/ThemeToggle.js.map +0 -1
- package/lib/typescript/molecules/ThemeToggle.d.ts.map +0 -1
- package/src/molecules/ThemeToggle.tsx +0 -67
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { memo, useMemo, useState } from 'react';
|
|
1
|
+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import { createStyles } from '../theme/createStyles';
|
|
4
|
-
import { useAllowFontScaling,
|
|
4
|
+
import { useAllowFontScaling, useLabels } from '../theme/UIProvider';
|
|
5
5
|
import { IconButton } from '../atoms/IconButton';
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_MONTH_NAMES = [
|
|
@@ -36,34 +36,40 @@ export interface DatePickerProps {
|
|
|
36
36
|
maxDate?: Date;
|
|
37
37
|
/** Mes mostrado al abrir si no hay `value`. @default hoy */
|
|
38
38
|
defaultMonth?: Date;
|
|
39
|
+
/** Nombres de los meses. @default los de `useLabels()` */
|
|
39
40
|
monthNames?: string[];
|
|
41
|
+
/** Iniciales de los días, índice 0 = domingo. @default las de `useLabels()` */
|
|
40
42
|
weekdayNames?: string[];
|
|
41
43
|
/** 0 = domingo, 1 = lunes. @default 0 */
|
|
42
44
|
firstDayOfWeek?: 0 | 1;
|
|
45
|
+
/** Avisa qué mes quedó a la vista al navegar con las flechas. */
|
|
46
|
+
onVisibleMonthChange?: (month: Date) => void;
|
|
43
47
|
style?: StyleProp<ViewStyle>;
|
|
44
48
|
testID?: string;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
/** Compara sólo año/mes/día, ignorando hora — dos Date del mismo día son "iguales". */
|
|
48
|
-
function isSameDay(a: Date, b: Date): boolean {
|
|
49
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
51
|
function startOfDay(d: Date): Date {
|
|
53
52
|
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
/** Timestamp del día, para comparar sin volver a crear Date en cada celda. */
|
|
56
|
+
function dayTime(d: Date): number {
|
|
57
|
+
return startOfDay(d).getTime();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function dayTimeOf(d: Date | undefined): number | undefined {
|
|
61
|
+
return d ? dayTime(d) : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Milisegundos que faltan para la próxima medianoche local. */
|
|
65
|
+
function msUntilNextMidnight(from: Date): number {
|
|
66
|
+
const next = new Date(from.getFullYear(), from.getMonth(), from.getDate() + 1);
|
|
67
|
+
return next.getTime() - from.getTime();
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (maxDate && t > startOfDay(maxDate).getTime()) return true;
|
|
66
|
-
return false;
|
|
70
|
+
/** Número de mes absoluto (año * 12 + mes), para comparar meses con un `<`. */
|
|
71
|
+
function monthIndex(d: Date): number {
|
|
72
|
+
return d.getFullYear() * 12 + d.getMonth();
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
/**
|
|
@@ -121,6 +127,74 @@ const useStyles = createStyles((theme) => ({
|
|
|
121
127
|
dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },
|
|
122
128
|
}));
|
|
123
129
|
|
|
130
|
+
type Styles = ReturnType<typeof useStyles>;
|
|
131
|
+
|
|
132
|
+
interface DayCellProps {
|
|
133
|
+
day: Date;
|
|
134
|
+
label: string;
|
|
135
|
+
selected: boolean;
|
|
136
|
+
inRange: boolean;
|
|
137
|
+
isRangeStart: boolean;
|
|
138
|
+
isRangeEnd: boolean;
|
|
139
|
+
isToday: boolean;
|
|
140
|
+
disabled: boolean;
|
|
141
|
+
allowFontScaling: boolean;
|
|
142
|
+
styles: Styles;
|
|
143
|
+
onPress: (day: Date) => void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Una celda del calendario. Va memoizada aparte porque el mes entero son 42
|
|
148
|
+
* celdas: sin esto, cualquier render del padre reconcilia las 42.
|
|
149
|
+
*/
|
|
150
|
+
const DayCell = memo(
|
|
151
|
+
({
|
|
152
|
+
day,
|
|
153
|
+
label,
|
|
154
|
+
selected,
|
|
155
|
+
inRange,
|
|
156
|
+
isRangeStart,
|
|
157
|
+
isRangeEnd,
|
|
158
|
+
isToday,
|
|
159
|
+
disabled,
|
|
160
|
+
allowFontScaling,
|
|
161
|
+
styles,
|
|
162
|
+
onPress,
|
|
163
|
+
}: DayCellProps) => (
|
|
164
|
+
<View
|
|
165
|
+
style={[
|
|
166
|
+
styles.cell,
|
|
167
|
+
(inRange || isRangeStart || isRangeEnd) && styles.cellInRange,
|
|
168
|
+
isRangeStart && styles.cellRangeStart,
|
|
169
|
+
isRangeEnd && styles.cellRangeEnd,
|
|
170
|
+
]}
|
|
171
|
+
>
|
|
172
|
+
<TouchableOpacity
|
|
173
|
+
style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}
|
|
174
|
+
onPress={() => onPress(day)}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
activeOpacity={0.7}
|
|
177
|
+
accessibilityRole="button"
|
|
178
|
+
accessibilityLabel={label}
|
|
179
|
+
accessibilityState={{ selected, disabled }}
|
|
180
|
+
>
|
|
181
|
+
<Text
|
|
182
|
+
allowFontScaling={allowFontScaling}
|
|
183
|
+
style={[
|
|
184
|
+
styles.dayText,
|
|
185
|
+
inRange && styles.dayTextInRange,
|
|
186
|
+
selected && styles.dayTextSelected,
|
|
187
|
+
disabled && styles.dayTextDisabled,
|
|
188
|
+
]}
|
|
189
|
+
>
|
|
190
|
+
{day.getDate()}
|
|
191
|
+
</Text>
|
|
192
|
+
</TouchableOpacity>
|
|
193
|
+
</View>
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
DayCell.displayName = 'DayCell';
|
|
197
|
+
|
|
124
198
|
/**
|
|
125
199
|
* DatePicker
|
|
126
200
|
* Calendario mensual dibujado con View/Text puros — sin
|
|
@@ -137,58 +211,117 @@ export const DatePicker = memo(
|
|
|
137
211
|
minDate,
|
|
138
212
|
maxDate,
|
|
139
213
|
defaultMonth,
|
|
140
|
-
monthNames
|
|
141
|
-
weekdayNames
|
|
214
|
+
monthNames,
|
|
215
|
+
weekdayNames,
|
|
142
216
|
firstDayOfWeek = 0,
|
|
217
|
+
onVisibleMonthChange,
|
|
143
218
|
style,
|
|
144
219
|
testID,
|
|
145
220
|
}: DatePickerProps) => {
|
|
146
|
-
const colors = useColors();
|
|
147
221
|
const styles = useStyles();
|
|
222
|
+
const labels = useLabels();
|
|
148
223
|
const allowFontScaling = useAllowFontScaling();
|
|
149
224
|
const isRange = mode === 'range';
|
|
150
225
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
226
|
+
const months = monthNames ?? labels.months;
|
|
227
|
+
const weekdays = weekdayNames ?? labels.weekdaysShort;
|
|
228
|
+
|
|
229
|
+
const handlePress = useCallback(
|
|
230
|
+
(day: Date) => {
|
|
231
|
+
if (!isRange) {
|
|
232
|
+
onChange?.(day);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Primer toque (o rango ya cerrado, o toque antes del inicio): arranca
|
|
236
|
+
// un rango nuevo. Segundo toque en adelante: lo cierra.
|
|
237
|
+
if (!range?.start || range.end || dayTime(day) < dayTime(range.start)) {
|
|
238
|
+
onRangeChange?.({ start: day });
|
|
239
|
+
} else {
|
|
240
|
+
onRangeChange?.({ start: range.start, end: day });
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
[isRange, onChange, onRangeChange, range?.start, range?.end]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const anchor = value ?? range?.start ?? defaultMonth;
|
|
247
|
+
const [visibleMonth, setVisibleMonth] = useState(
|
|
248
|
+
() => new Date((anchor ?? new Date()).getFullYear(), (anchor ?? new Date()).getMonth(), 1)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// La fecha elegida puede cambiar desde afuera (uso controlado): sin esto,
|
|
252
|
+
// el calendario se quedaba clavado en el mes con el que montó.
|
|
253
|
+
const anchorMonth = anchor ? monthIndex(anchor) : undefined;
|
|
254
|
+
const lastAnchorMonth = useRef(anchorMonth);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (anchorMonth === undefined || anchorMonth === lastAnchorMonth.current) return;
|
|
257
|
+
lastAnchorMonth.current = anchorMonth;
|
|
258
|
+
setVisibleMonth(new Date(Math.floor(anchorMonth / 12), anchorMonth % 12, 1));
|
|
259
|
+
}, [anchorMonth]);
|
|
260
|
+
|
|
261
|
+
// "Hoy" se recalcula en la próxima medianoche: una app que queda abierta
|
|
262
|
+
// toda la noche marcaba el día anterior.
|
|
263
|
+
const [todayTs, setTodayTs] = useState(() => dayTime(new Date()));
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
const timer = setTimeout(() => setTodayTs(dayTime(new Date())), msUntilNextMidnight(new Date()));
|
|
266
|
+
return () => clearTimeout(timer);
|
|
267
|
+
}, [todayTs]);
|
|
268
|
+
|
|
169
269
|
const grid = useMemo(
|
|
170
270
|
() => buildMonthGrid(visibleMonth.getFullYear(), visibleMonth.getMonth(), firstDayOfWeek),
|
|
171
271
|
[visibleMonth, firstDayOfWeek]
|
|
172
272
|
);
|
|
173
273
|
|
|
174
274
|
const orderedWeekdays = useMemo(
|
|
175
|
-
() => (firstDayOfWeek === 0 ?
|
|
176
|
-
[
|
|
275
|
+
() => (firstDayOfWeek === 0 ? weekdays : [...weekdays.slice(1), weekdays[0]!]),
|
|
276
|
+
[weekdays, firstDayOfWeek]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Los límites se resuelven una vez y no 42 veces por render: comparar
|
|
280
|
+
// timestamps sale gratis, crear un Date por celda no.
|
|
281
|
+
const bounds = useMemo(
|
|
282
|
+
() => ({
|
|
283
|
+
min: dayTimeOf(minDate),
|
|
284
|
+
max: dayTimeOf(maxDate),
|
|
285
|
+
start: dayTimeOf(range?.start),
|
|
286
|
+
end: dayTimeOf(range?.end),
|
|
287
|
+
selected: dayTimeOf(value),
|
|
288
|
+
}),
|
|
289
|
+
[minDate, maxDate, range?.start, range?.end, value]
|
|
177
290
|
);
|
|
178
291
|
|
|
179
292
|
const shiftMonth = (delta: number) =>
|
|
180
|
-
setVisibleMonth((m) =>
|
|
293
|
+
setVisibleMonth((m) => {
|
|
294
|
+
const next = new Date(m.getFullYear(), m.getMonth() + delta, 1);
|
|
295
|
+
onVisibleMonthChange?.(next);
|
|
296
|
+
return next;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Un mes entero fuera de los límites no tiene nada que elegir: la flecha
|
|
300
|
+
// que lleva ahí se apaga en vez de dejar navegar al vacío.
|
|
301
|
+
const visibleIndex = monthIndex(visibleMonth);
|
|
302
|
+
const canGoBack = !minDate || visibleIndex > monthIndex(minDate);
|
|
303
|
+
const canGoForward = !maxDate || visibleIndex < monthIndex(maxDate);
|
|
181
304
|
|
|
182
|
-
const monthLabel = `${
|
|
305
|
+
const monthLabel = `${months[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;
|
|
183
306
|
|
|
184
307
|
return (
|
|
185
308
|
<View style={[styles.container, style]} testID={testID}>
|
|
186
309
|
<View style={styles.header}>
|
|
187
|
-
<IconButton
|
|
310
|
+
<IconButton
|
|
311
|
+
iconName="chevron-left"
|
|
312
|
+
accessibilityLabel="Previous month"
|
|
313
|
+
disabled={!canGoBack}
|
|
314
|
+
onPress={() => shiftMonth(-1)}
|
|
315
|
+
/>
|
|
188
316
|
<Text allowFontScaling={allowFontScaling} style={styles.monthLabel} accessibilityRole="header">
|
|
189
317
|
{monthLabel}
|
|
190
318
|
</Text>
|
|
191
|
-
<IconButton
|
|
319
|
+
<IconButton
|
|
320
|
+
iconName="chevron-right"
|
|
321
|
+
accessibilityLabel="Next month"
|
|
322
|
+
disabled={!canGoForward}
|
|
323
|
+
onPress={() => shiftMonth(1)}
|
|
324
|
+
/>
|
|
192
325
|
</View>
|
|
193
326
|
|
|
194
327
|
<View style={styles.weekRow}>
|
|
@@ -204,46 +337,31 @@ export const DatePicker = memo(
|
|
|
204
337
|
{row.map((day, cellIndex) => {
|
|
205
338
|
if (!day) return <View style={styles.cell} key={cellIndex} />;
|
|
206
339
|
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
const
|
|
340
|
+
const t = dayTime(day);
|
|
341
|
+
const disabled = (bounds.min !== undefined && t < bounds.min) || (bounds.max !== undefined && t > bounds.max);
|
|
342
|
+
const isStart = isRange && bounds.start === t;
|
|
343
|
+
const isEnd = isRange && bounds.end === t;
|
|
344
|
+
const closed = isRange && bounds.start !== undefined && bounds.end !== undefined;
|
|
345
|
+
const between = closed && t > bounds.start! && t < bounds.end!;
|
|
346
|
+
const selected = isRange ? isStart || isEnd : bounds.selected === t;
|
|
212
347
|
|
|
213
|
-
const disabled = isOutOfRange(day, minDate, maxDate);
|
|
214
|
-
const isToday = isSameDay(day, today);
|
|
215
348
|
return (
|
|
216
|
-
<
|
|
217
|
-
style={[
|
|
218
|
-
styles.cell,
|
|
219
|
-
(inRange || (isStart && !!range?.end) || isEnd) && styles.cellInRange,
|
|
220
|
-
isStart && !!range?.end && styles.cellRangeStart,
|
|
221
|
-
isEnd && styles.cellRangeEnd,
|
|
222
|
-
]}
|
|
349
|
+
<DayCell
|
|
223
350
|
key={cellIndex}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
inRange && styles.dayTextInRange,
|
|
239
|
-
selected && styles.dayTextSelected,
|
|
240
|
-
disabled && styles.dayTextDisabled,
|
|
241
|
-
]}
|
|
242
|
-
>
|
|
243
|
-
{day.getDate()}
|
|
244
|
-
</Text>
|
|
245
|
-
</TouchableOpacity>
|
|
246
|
-
</View>
|
|
351
|
+
day={day}
|
|
352
|
+
label={`${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}
|
|
353
|
+
selected={selected}
|
|
354
|
+
// Un día fuera de los límites no se pinta como parte del
|
|
355
|
+
// rango: se ve deshabilitado, y eso es lo que es.
|
|
356
|
+
inRange={between && !disabled}
|
|
357
|
+
isRangeStart={!!closed && isStart && !disabled}
|
|
358
|
+
isRangeEnd={isEnd && !disabled}
|
|
359
|
+
isToday={t === todayTs}
|
|
360
|
+
disabled={disabled}
|
|
361
|
+
allowFontScaling={allowFontScaling}
|
|
362
|
+
styles={styles}
|
|
363
|
+
onPress={handlePress}
|
|
364
|
+
/>
|
|
247
365
|
);
|
|
248
366
|
})}
|
|
249
367
|
</View>
|
package/src/organisms/List.tsx
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
ActivityIndicator,
|
|
4
|
-
FlatList,
|
|
5
|
-
Platform,
|
|
6
|
-
RefreshControl,
|
|
7
|
-
Text,
|
|
8
|
-
View,
|
|
9
|
-
type FlatListProps,
|
|
10
|
-
type ViewStyle,
|
|
11
|
-
} from 'react-native';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { FlatList, RefreshControl, View, type FlatListProps, type ViewStyle } from 'react-native';
|
|
12
3
|
import { createStyles } from '../theme/createStyles';
|
|
13
4
|
import { useColors, useLabels } from '../theme/UIProvider';
|
|
14
5
|
import { EmptyState } from '../atoms/EmptyState';
|
|
6
|
+
import { Skeleton } from '../atoms/Skeleton';
|
|
15
7
|
import { Spinner } from '../atoms/Spinner';
|
|
16
8
|
import type { IconName } from '../icons/types';
|
|
17
9
|
|
|
18
10
|
export interface ListProps<T> {
|
|
19
11
|
data: T[];
|
|
20
|
-
keyExtractor: (item: T) => string;
|
|
12
|
+
keyExtractor: (item: T, index: number) => string;
|
|
21
13
|
renderItem: FlatListProps<T>['renderItem'];
|
|
22
14
|
|
|
15
|
+
/** Carga inicial: mientras sea true y no haya datos, se muestran placeholders. */
|
|
23
16
|
isLoading: boolean;
|
|
17
|
+
/** Recarga con datos ya en pantalla (pull-to-refresh). @default false */
|
|
18
|
+
isRefreshing?: boolean;
|
|
24
19
|
isLoadingMore?: boolean;
|
|
25
20
|
/** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */
|
|
26
21
|
isReady?: boolean;
|
|
27
22
|
loadingText?: string;
|
|
28
23
|
loadingMoreText?: string;
|
|
29
24
|
|
|
25
|
+
/** Filas fantasma de la carga inicial. @default 6 */
|
|
26
|
+
skeletonCount?: number;
|
|
27
|
+
/** Placeholder propio, para que calce con la forma real del ítem. */
|
|
28
|
+
renderSkeletonItem?: () => React.ReactElement;
|
|
29
|
+
|
|
30
30
|
onEndReached?: () => void;
|
|
31
31
|
onEndReachedThreshold?: number;
|
|
32
32
|
onRefresh?: () => void;
|
|
@@ -48,20 +48,15 @@ export interface ListProps<T> {
|
|
|
48
48
|
|
|
49
49
|
const useStyles = createStyles((theme) => ({
|
|
50
50
|
container: { flex: 1, backgroundColor: theme.colors.background },
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
right: 0,
|
|
56
|
-
bottom: 0,
|
|
57
|
-
backgroundColor: theme.colors.scrim,
|
|
58
|
-
zIndex: 10,
|
|
59
|
-
justifyContent: 'center',
|
|
51
|
+
listContent: { padding: theme.tokens.spacing.md },
|
|
52
|
+
footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },
|
|
53
|
+
skeletonRow: {
|
|
54
|
+
flexDirection: 'row',
|
|
60
55
|
alignItems: 'center',
|
|
56
|
+
gap: theme.tokens.spacing.md,
|
|
57
|
+
padding: theme.tokens.spacing.md,
|
|
61
58
|
},
|
|
62
|
-
|
|
63
|
-
listContent: { padding: theme.tokens.spacing.md, paddingBottom: Platform.OS === 'android' ? 100 : 80 },
|
|
64
|
-
footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },
|
|
59
|
+
skeletonText: { flex: 1, gap: theme.tokens.spacing.sm },
|
|
65
60
|
}));
|
|
66
61
|
|
|
67
62
|
/**
|
|
@@ -75,10 +70,13 @@ export function List<T>({
|
|
|
75
70
|
keyExtractor,
|
|
76
71
|
renderItem,
|
|
77
72
|
isLoading,
|
|
73
|
+
isRefreshing = false,
|
|
78
74
|
isLoadingMore = false,
|
|
79
75
|
isReady = true,
|
|
80
76
|
loadingText,
|
|
81
77
|
loadingMoreText,
|
|
78
|
+
skeletonCount = 6,
|
|
79
|
+
renderSkeletonItem,
|
|
82
80
|
onEndReached,
|
|
83
81
|
onEndReachedThreshold = 0.3,
|
|
84
82
|
onRefresh,
|
|
@@ -98,22 +96,36 @@ export function List<T>({
|
|
|
98
96
|
const resolvedLoadingText = loadingText ?? labels.loading;
|
|
99
97
|
const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;
|
|
100
98
|
|
|
99
|
+
const isInitialLoading = isLoading && data.length === 0;
|
|
100
|
+
|
|
101
|
+
// El footer propio y el loader de paginación conviven: antes el primero
|
|
102
|
+
// tapaba al segundo y la lista dejaba de avisar que estaba trayendo más.
|
|
101
103
|
const renderFooter = useCallback(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const custom =
|
|
105
|
+
typeof ListFooterComponent === 'function' ? (
|
|
106
|
+
<ListFooterComponent />
|
|
107
|
+
) : (
|
|
108
|
+
(ListFooterComponent as React.ReactElement | undefined) ?? null
|
|
109
|
+
);
|
|
110
|
+
|
|
106
111
|
return (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
<>
|
|
113
|
+
{custom}
|
|
114
|
+
{isLoadingMore && (
|
|
115
|
+
<View style={styles.footerLoader}>
|
|
116
|
+
<Spinner text={resolvedLoadingMoreText} fullscreen={false} />
|
|
117
|
+
</View>
|
|
118
|
+
)}
|
|
119
|
+
</>
|
|
110
120
|
);
|
|
111
121
|
}, [isLoadingMore, resolvedLoadingMoreText, ListFooterComponent, styles.footerLoader]);
|
|
112
122
|
|
|
123
|
+
// Vacío sólo cuando ya se sabe que no hay nada: durante la carga inicial se
|
|
124
|
+
// muestran placeholders, no "sin resultados".
|
|
113
125
|
const renderEmpty = useCallback(() => {
|
|
114
|
-
if (isLoading) return null;
|
|
126
|
+
if (isLoading || isRefreshing) return null;
|
|
115
127
|
return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;
|
|
116
|
-
}, [isLoading, emptyTitle, emptyDescription, emptyIconName]);
|
|
128
|
+
}, [isLoading, isRefreshing, emptyTitle, emptyDescription, emptyIconName]);
|
|
117
129
|
|
|
118
130
|
if (!isReady) {
|
|
119
131
|
return (
|
|
@@ -123,15 +135,30 @@ export function List<T>({
|
|
|
123
135
|
);
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
<View style={styles.
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
if (isInitialLoading) {
|
|
139
|
+
return (
|
|
140
|
+
<View style={[styles.container, containerStyle]} testID="list-skeleton">
|
|
141
|
+
<View style={[styles.listContent, contentContainerStyle]}>
|
|
142
|
+
{Array.from({ length: skeletonCount }, (_, i) => (
|
|
143
|
+
<React.Fragment key={i}>
|
|
144
|
+
{renderSkeletonItem?.() ?? (
|
|
145
|
+
<View style={styles.skeletonRow}>
|
|
146
|
+
<Skeleton variant="circle" width={40} />
|
|
147
|
+
<View style={styles.skeletonText}>
|
|
148
|
+
<Skeleton width="60%" height={14} />
|
|
149
|
+
<Skeleton width="35%" height={12} />
|
|
150
|
+
</View>
|
|
151
|
+
</View>
|
|
152
|
+
)}
|
|
153
|
+
</React.Fragment>
|
|
154
|
+
))}
|
|
132
155
|
</View>
|
|
133
|
-
|
|
156
|
+
</View>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
134
159
|
|
|
160
|
+
return (
|
|
161
|
+
<View style={[styles.container, containerStyle]}>
|
|
135
162
|
<ListComponent
|
|
136
163
|
data={data}
|
|
137
164
|
keyExtractor={keyExtractor}
|
|
@@ -144,7 +171,12 @@ export function List<T>({
|
|
|
144
171
|
ListEmptyComponent={renderEmpty}
|
|
145
172
|
refreshControl={
|
|
146
173
|
onRefresh ? (
|
|
147
|
-
<RefreshControl
|
|
174
|
+
<RefreshControl
|
|
175
|
+
refreshing={isRefreshing}
|
|
176
|
+
onRefresh={onRefresh}
|
|
177
|
+
colors={[colors.primary]}
|
|
178
|
+
tintColor={colors.primary}
|
|
179
|
+
/>
|
|
148
180
|
) : undefined
|
|
149
181
|
}
|
|
150
182
|
{...listProps}
|