orn-ui 0.2.0 → 0.2.2

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 (54) hide show
  1. package/README.md +16 -5
  2. package/lib/commonjs/molecules/Steps.js +36 -6
  3. package/lib/commonjs/molecules/Steps.js.map +1 -1
  4. package/lib/commonjs/organisms/DateField.js +46 -15
  5. package/lib/commonjs/organisms/DateField.js.map +1 -1
  6. package/lib/commonjs/organisms/DatePicker.js +129 -50
  7. package/lib/commonjs/organisms/DatePicker.js.map +1 -1
  8. package/lib/commonjs/organisms/NavigationBar.js +6 -1
  9. package/lib/commonjs/organisms/NavigationBar.js.map +1 -1
  10. package/lib/commonjs/organisms/SearchList.js +1 -2
  11. package/lib/commonjs/organisms/SearchList.js.map +1 -1
  12. package/lib/commonjs/organisms/Wizard.js +1 -0
  13. package/lib/commonjs/organisms/Wizard.js.map +1 -1
  14. package/lib/commonjs/theme/labels.js +5 -1
  15. package/lib/commonjs/theme/labels.js.map +1 -1
  16. package/lib/module/molecules/Steps.js +37 -7
  17. package/lib/module/molecules/Steps.js.map +1 -1
  18. package/lib/module/organisms/DateField.js +49 -18
  19. package/lib/module/organisms/DateField.js.map +1 -1
  20. package/lib/module/organisms/DatePicker.js +131 -52
  21. package/lib/module/organisms/DatePicker.js.map +1 -1
  22. package/lib/module/organisms/NavigationBar.js +6 -1
  23. package/lib/module/organisms/NavigationBar.js.map +1 -1
  24. package/lib/module/organisms/SearchList.js +1 -2
  25. package/lib/module/organisms/SearchList.js.map +1 -1
  26. package/lib/module/organisms/Wizard.js +1 -0
  27. package/lib/module/organisms/Wizard.js.map +1 -1
  28. package/lib/module/theme/labels.js +5 -1
  29. package/lib/module/theme/labels.js.map +1 -1
  30. package/lib/typescript/molecules/Steps.d.ts.map +1 -1
  31. package/lib/typescript/organisms/DateField.d.ts +23 -5
  32. package/lib/typescript/organisms/DateField.d.ts.map +1 -1
  33. package/lib/typescript/organisms/DatePicker.d.ts +5 -1
  34. package/lib/typescript/organisms/DatePicker.d.ts.map +1 -1
  35. package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -1
  36. package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
  37. package/lib/typescript/theme/labels.d.ts +6 -0
  38. package/lib/typescript/theme/labels.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/registry/core.json +1 -1
  41. package/registry/date-field.json +1 -1
  42. package/registry/date-picker.json +1 -1
  43. package/registry/index.json +7 -7
  44. package/registry/navigation-bar.json +1 -1
  45. package/registry/search-list.json +1 -1
  46. package/registry/steps.json +1 -1
  47. package/registry/wizard.json +1 -1
  48. package/src/molecules/Steps.tsx +27 -8
  49. package/src/organisms/DateField.tsx +79 -19
  50. package/src/organisms/DatePicker.tsx +198 -80
  51. package/src/organisms/NavigationBar.tsx +3 -0
  52. package/src/organisms/SearchList.tsx +1 -1
  53. package/src/organisms/Wizard.tsx +1 -0
  54. 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, DEFAULT_MONTH_NAMES, type DatePickerProps } from './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
- onChange: (date: Date) => void;
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 "D de Month de YYYY" en inglés */
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 'Select date' */
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 = 'Select date',
68
- clearLabel = 'Clear',
88
+ modalTitle,
89
+ onClear,
90
+ clearLabel,
69
91
  containerStyle,
70
- monthNames = DEFAULT_MONTH_NAMES,
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 display = value ? (format ? format(value) : defaultFormat(value, monthNames)) : placeholder;
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, !value && styles.placeholder]}>
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={<Button title={clearLabel} variant="ghost" onPress={() => setOpen(false)} />}
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
- monthNames={monthNames}
183
+ range={range}
184
+ monthNames={months}
123
185
  {...pickerProps}
124
- onChange={(date) => {
125
- onChange(date);
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, useColors } from '../theme/UIProvider';
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
- /** Estrictamente entre start y end, sin incluir los extremos. */
57
- function isBetween(day: Date, start: Date, end: Date): boolean {
58
- const t = startOfDay(day).getTime();
59
- return t > startOfDay(start).getTime() && t < startOfDay(end).getTime();
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
- function isOutOfRange(day: Date, minDate?: Date, maxDate?: Date): boolean {
63
- const t = startOfDay(day).getTime();
64
- if (minDate && t < startOfDay(minDate).getTime()) return true;
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 = DEFAULT_MONTH_NAMES,
141
- weekdayNames = DEFAULT_WEEKDAY_NAMES,
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 handlePress = (day: Date) => {
152
- if (!isRange) {
153
- onChange?.(day);
154
- return;
155
- }
156
- // Primer toque (o rango ya cerrado, o toque antes del inicio): arranca
157
- // un rango nuevo. Segundo toque en adelante: lo cierra.
158
- if (!range?.start || range.end || startOfDay(day) < startOfDay(range.start)) {
159
- onRangeChange?.({ start: day });
160
- } else {
161
- onRangeChange?.({ start: range.start, end: day });
162
- }
163
- };
164
-
165
- const initial = value ?? range?.start ?? defaultMonth ?? new Date();
166
- const [visibleMonth, setVisibleMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));
167
-
168
- const today = useMemo(() => new Date(), []);
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 ? weekdayNames : [...weekdayNames.slice(1), weekdayNames[0]!]),
176
- [weekdayNames, firstDayOfWeek]
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) => new Date(m.getFullYear(), m.getMonth() + delta, 1));
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 = `${monthNames[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;
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 iconName="chevron-left" accessibilityLabel="Previous month" onPress={() => shiftMonth(-1)} />
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 iconName="chevron-right" accessibilityLabel="Next month" onPress={() => shiftMonth(1)} />
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 isStart = isRange && !!range?.start && isSameDay(day, range.start);
208
- const isEnd = isRange && !!range?.end && isSameDay(day, range.end);
209
- const inRange =
210
- isRange && !!range?.start && !!range.end && isBetween(day, range.start, range.end);
211
- const selected = isRange ? isStart || isEnd : !!value && isSameDay(day, value);
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
- <View
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
- <TouchableOpacity
226
- style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}
227
- onPress={() => handlePress(day)}
228
- disabled={disabled}
229
- activeOpacity={0.7}
230
- accessibilityRole="button"
231
- accessibilityLabel={`${monthNames[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}
232
- accessibilityState={{ selected, disabled }}
233
- >
234
- <Text
235
- allowFontScaling={allowFontScaling}
236
- style={[
237
- styles.dayText,
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, backgroundColor: theme.colors.background },
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 },
@@ -142,6 +142,7 @@ export function Wizard({
142
142
  de los campos. */}
143
143
  {scrollableContent ? (
144
144
  <ScrollView
145
+ testID="wizard-content"
145
146
  style={styles.content}
146
147
  contentContainerStyle={styles.contentInner}
147
148
  keyboardShouldPersistTaps="handled"
@@ -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
  };