orn-ui 0.2.0 → 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 +11 -0
- package/lib/commonjs/molecules/Steps.js +36 -6
- package/lib/commonjs/molecules/Steps.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/NavigationBar.js +6 -1
- package/lib/commonjs/organisms/NavigationBar.js.map +1 -1
- package/lib/commonjs/organisms/SearchList.js +1 -2
- package/lib/commonjs/organisms/SearchList.js.map +1 -1
- package/lib/commonjs/organisms/Wizard.js +1 -0
- package/lib/commonjs/organisms/Wizard.js.map +1 -1
- package/lib/commonjs/theme/labels.js +5 -1
- package/lib/commonjs/theme/labels.js.map +1 -1
- package/lib/module/molecules/Steps.js +37 -7
- package/lib/module/molecules/Steps.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/NavigationBar.js +6 -1
- package/lib/module/organisms/NavigationBar.js.map +1 -1
- package/lib/module/organisms/SearchList.js +1 -2
- package/lib/module/organisms/SearchList.js.map +1 -1
- package/lib/module/organisms/Wizard.js +1 -0
- package/lib/module/organisms/Wizard.js.map +1 -1
- package/lib/module/theme/labels.js +5 -1
- package/lib/module/theme/labels.js.map +1 -1
- package/lib/typescript/molecules/Steps.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/NavigationBar.d.ts.map +1 -1
- package/lib/typescript/organisms/Wizard.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/package.json +1 -1
- package/registry/core.json +1 -1
- package/registry/date-field.json +1 -1
- package/registry/date-picker.json +1 -1
- package/registry/index.json +7 -7
- package/registry/navigation-bar.json +1 -1
- package/registry/search-list.json +1 -1
- package/registry/steps.json +1 -1
- package/registry/wizard.json +1 -1
- package/src/molecules/Steps.tsx +27 -8
- package/src/organisms/DateField.tsx +79 -19
- package/src/organisms/DatePicker.tsx +198 -80
- package/src/organisms/NavigationBar.tsx +3 -0
- package/src/organisms/SearchList.tsx +1 -1
- package/src/organisms/Wizard.tsx +1 -0
- package/src/theme/labels.ts +13 -0
|
@@ -1,26 +1,44 @@
|
|
|
1
|
-
import React, { memo, useState } from 'react';
|
|
1
|
+
import React, { memo, useCallback, 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, useColors } from '../theme/UIProvider';
|
|
4
|
+
import { useAllowFontScaling, useColors, useLabels } from '../theme/UIProvider';
|
|
5
5
|
import { Modal } from './Modal';
|
|
6
|
-
import { DatePicker,
|
|
6
|
+
import { DatePicker, type DatePickerProps, type DateRange } from './DatePicker';
|
|
7
7
|
import { Button } from '../atoms/Button';
|
|
8
8
|
|
|
9
9
|
export interface DateFieldProps
|
|
10
10
|
extends Pick<DatePickerProps, 'minDate' | 'maxDate' | 'monthNames' | 'weekdayNames' | 'firstDayOfWeek'> {
|
|
11
11
|
label?: string;
|
|
12
12
|
required?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 'single' elige una fecha; 'range' elige un intervalo con dos toques.
|
|
15
|
+
* @default 'single'
|
|
16
|
+
*/
|
|
17
|
+
mode?: 'single' | 'range';
|
|
18
|
+
/** Fecha elegida en mode="single". */
|
|
13
19
|
value?: Date;
|
|
14
|
-
|
|
20
|
+
/** Se llama en mode="single". */
|
|
21
|
+
onChange?: (date: Date) => void;
|
|
22
|
+
/** Rango elegido en mode="range". */
|
|
23
|
+
range?: DateRange;
|
|
24
|
+
/** Se llama en mode="range", tanto al fijar el inicio como al cerrar el rango. */
|
|
25
|
+
onRangeChange?: (range: DateRange) => void;
|
|
15
26
|
placeholder?: string;
|
|
16
27
|
error?: string;
|
|
17
28
|
disabled?: boolean;
|
|
18
|
-
/** Formatea la fecha en el campo. @default "
|
|
29
|
+
/** Formatea la fecha en el campo. @default "Month D, YYYY" */
|
|
19
30
|
format?: (date: Date) => string;
|
|
20
|
-
/** Título del modal del calendario. @default
|
|
31
|
+
/** Título del modal del calendario. @default el `selectDate` de `useLabels()` */
|
|
21
32
|
modalTitle?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Si se pasa, el modal muestra un botón que borra la selección y cierra.
|
|
35
|
+
* Sin esto no hay botón: el "Clear" anterior sólo cerraba, sin limpiar nada.
|
|
36
|
+
*/
|
|
37
|
+
onClear?: () => void;
|
|
38
|
+
/** Texto de ese botón. @default el `clear` de `useLabels()` */
|
|
22
39
|
clearLabel?: string;
|
|
23
40
|
containerStyle?: StyleProp<ViewStyle>;
|
|
41
|
+
testID?: string;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
function defaultFormat(date: Date, monthNames: string[]): string {
|
|
@@ -58,27 +76,67 @@ export const DateField = memo(
|
|
|
58
76
|
({
|
|
59
77
|
label,
|
|
60
78
|
required = false,
|
|
79
|
+
mode = 'single',
|
|
61
80
|
value,
|
|
62
81
|
onChange,
|
|
82
|
+
range,
|
|
83
|
+
onRangeChange,
|
|
63
84
|
placeholder = 'Select a date',
|
|
64
85
|
error,
|
|
65
86
|
disabled = false,
|
|
66
87
|
format,
|
|
67
|
-
modalTitle
|
|
68
|
-
|
|
88
|
+
modalTitle,
|
|
89
|
+
onClear,
|
|
90
|
+
clearLabel,
|
|
69
91
|
containerStyle,
|
|
70
|
-
monthNames
|
|
92
|
+
monthNames,
|
|
93
|
+
testID,
|
|
71
94
|
...pickerProps
|
|
72
95
|
}: DateFieldProps) => {
|
|
73
96
|
const colors = useColors();
|
|
74
97
|
const styles = useStyles();
|
|
98
|
+
const labels = useLabels();
|
|
75
99
|
const allowFontScaling = useAllowFontScaling();
|
|
76
100
|
const [open, setOpen] = useState(false);
|
|
77
101
|
|
|
78
|
-
const
|
|
102
|
+
const months = monthNames ?? labels.months;
|
|
103
|
+
const isRange = mode === 'range';
|
|
104
|
+
|
|
105
|
+
const formatDate = (date: Date) => (format ? format(date) : defaultFormat(date, months));
|
|
106
|
+
|
|
107
|
+
// El calendario se cierra solo cuando ya no falta nada por elegir: en
|
|
108
|
+
// rango, recién cuando el segundo toque cierra el intervalo.
|
|
109
|
+
const handleChange = useCallback(
|
|
110
|
+
(date: Date) => {
|
|
111
|
+
onChange?.(date);
|
|
112
|
+
setOpen(false);
|
|
113
|
+
},
|
|
114
|
+
[onChange]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const handleRangeChange = useCallback(
|
|
118
|
+
(next: DateRange) => {
|
|
119
|
+
onRangeChange?.(next);
|
|
120
|
+
if (next.end) setOpen(false);
|
|
121
|
+
},
|
|
122
|
+
[onRangeChange]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const handleClear = useCallback(() => {
|
|
126
|
+
onClear?.();
|
|
127
|
+
setOpen(false);
|
|
128
|
+
}, [onClear]);
|
|
129
|
+
|
|
130
|
+
const hasValue = isRange ? !!range?.start : !!value;
|
|
131
|
+
let display = placeholder;
|
|
132
|
+
if (isRange && range?.start) {
|
|
133
|
+
display = range.end ? `${formatDate(range.start)} — ${formatDate(range.end)}` : formatDate(range.start);
|
|
134
|
+
} else if (!isRange && value) {
|
|
135
|
+
display = formatDate(value);
|
|
136
|
+
}
|
|
79
137
|
|
|
80
138
|
return (
|
|
81
|
-
<View style={[styles.container, containerStyle]}>
|
|
139
|
+
<View style={[styles.container, containerStyle]} testID={testID}>
|
|
82
140
|
{!!label && (
|
|
83
141
|
<Text allowFontScaling={allowFontScaling} style={styles.label}>
|
|
84
142
|
{label}
|
|
@@ -95,7 +153,7 @@ export const DateField = memo(
|
|
|
95
153
|
accessibilityLabel={label ?? placeholder}
|
|
96
154
|
accessibilityState={{ disabled, expanded: open }}
|
|
97
155
|
>
|
|
98
|
-
<Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !
|
|
156
|
+
<Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !hasValue && styles.placeholder]}>
|
|
99
157
|
{display}
|
|
100
158
|
</Text>
|
|
101
159
|
<Text allowFontScaling={allowFontScaling} style={{ color: colors.textLight }}>
|
|
@@ -112,19 +170,21 @@ export const DateField = memo(
|
|
|
112
170
|
<Modal
|
|
113
171
|
visible={open}
|
|
114
172
|
onClose={() => setOpen(false)}
|
|
115
|
-
title={modalTitle}
|
|
173
|
+
title={modalTitle ?? labels.selectDate}
|
|
116
174
|
variant="overlay"
|
|
117
175
|
scrollable={false}
|
|
118
|
-
footer={
|
|
176
|
+
footer={
|
|
177
|
+
onClear ? <Button title={clearLabel ?? labels.clear} variant="ghost" onPress={handleClear} /> : undefined
|
|
178
|
+
}
|
|
119
179
|
>
|
|
120
180
|
<DatePicker
|
|
181
|
+
mode={mode}
|
|
121
182
|
value={value}
|
|
122
|
-
|
|
183
|
+
range={range}
|
|
184
|
+
monthNames={months}
|
|
123
185
|
{...pickerProps}
|
|
124
|
-
onChange={
|
|
125
|
-
|
|
126
|
-
setOpen(false);
|
|
127
|
-
}}
|
|
186
|
+
onChange={handleChange}
|
|
187
|
+
onRangeChange={handleRangeChange}
|
|
128
188
|
/>
|
|
129
189
|
</Modal>
|
|
130
190
|
</View>
|
|
@@ -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>
|
|
@@ -158,9 +158,12 @@ export const NavigationBar = memo(
|
|
|
158
158
|
>
|
|
159
159
|
{itemWidth > 0 && (
|
|
160
160
|
<Animated.View
|
|
161
|
+
testID="navigation-bar-indicator"
|
|
161
162
|
pointerEvents="none"
|
|
162
163
|
style={[
|
|
163
164
|
styles.indicator,
|
|
165
|
+
safeArea && position === 'bottom' && { bottom: theme.tokens.spacing.xs + insets.bottom },
|
|
166
|
+
safeArea && position === 'top' && { top: theme.tokens.spacing.xs + insets.top },
|
|
164
167
|
{
|
|
165
168
|
width: itemWidth - theme.tokens.spacing.sm * 2,
|
|
166
169
|
transform: [{ translateX: slide }],
|
|
@@ -74,7 +74,7 @@ export interface SearchListProps<T> {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const useStyles = createStyles((theme) => ({
|
|
77
|
-
container: { flex: 1
|
|
77
|
+
container: { flex: 1 },
|
|
78
78
|
searchRow: { flexDirection: 'row', alignItems: 'center', marginBottom: theme.tokens.spacing.lg - 1, gap: theme.tokens.spacing.sm + 2 },
|
|
79
79
|
searchInput: { flex: 1, marginBottom: 0 },
|
|
80
80
|
scanButton: { backgroundColor: theme.colors.primary, width: 50, height: 50, borderRadius: theme.tokens.radius.lg, ...theme.tokens.shadow.sm },
|
package/src/organisms/Wizard.tsx
CHANGED
package/src/theme/labels.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface Labels {
|
|
|
8
8
|
noResultsDescription: string;
|
|
9
9
|
search: string;
|
|
10
10
|
selectPlaceholder: string;
|
|
11
|
+
/** Nombres de los meses, de enero a diciembre. */
|
|
12
|
+
months: string[];
|
|
13
|
+
/** Iniciales de los días, índice 0 = domingo (alinea con `Date.getDay()`). */
|
|
14
|
+
weekdaysShort: string[];
|
|
15
|
+
selectDate: string;
|
|
16
|
+
clear: string;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export const defaultLabels: Labels = {
|
|
@@ -20,4 +26,11 @@ export const defaultLabels: Labels = {
|
|
|
20
26
|
noResultsDescription: 'Try a different search term',
|
|
21
27
|
search: 'Search...',
|
|
22
28
|
selectPlaceholder: 'Select...',
|
|
29
|
+
months: [
|
|
30
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
31
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
32
|
+
],
|
|
33
|
+
weekdaysShort: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
|
|
34
|
+
selectDate: 'Select date',
|
|
35
|
+
clear: 'Clear',
|
|
23
36
|
};
|