orn-ui 0.1.0 → 0.2.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.
- package/README.md +9 -2
- package/lib/commonjs/atoms/Avatar.js +1 -1
- package/lib/commonjs/atoms/Avatar.js.map +1 -1
- package/lib/commonjs/atoms/Badge.js +10 -8
- package/lib/commonjs/atoms/Badge.js.map +1 -1
- package/lib/commonjs/atoms/Button.js +8 -6
- package/lib/commonjs/atoms/Button.js.map +1 -1
- package/lib/commonjs/atoms/Checkbox.js +4 -1
- package/lib/commonjs/atoms/Checkbox.js.map +1 -1
- package/lib/commonjs/atoms/Fab.js +1 -1
- package/lib/commonjs/atoms/Fab.js.map +1 -1
- package/lib/commonjs/atoms/Image.js +2 -2
- package/lib/commonjs/atoms/Image.js.map +1 -1
- package/lib/commonjs/atoms/Input.js +2 -2
- package/lib/commonjs/atoms/Input.js.map +1 -1
- 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/AvatarHeader.js +2 -2
- package/lib/commonjs/molecules/AvatarHeader.js.map +1 -1
- package/lib/commonjs/molecules/OptionCard.js +57 -24
- package/lib/commonjs/molecules/OptionCard.js.map +1 -1
- package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
- 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 +2 -2
- 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/Alert.js +6 -5
- package/lib/commonjs/organisms/Alert.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 +2 -2
- package/lib/commonjs/organisms/DateField.js.map +1 -1
- package/lib/commonjs/organisms/DatePicker.js +3 -3
- 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 +205 -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 +99 -30
- package/lib/commonjs/organisms/SearchList.js.map +1 -1
- package/lib/commonjs/organisms/Select.js +226 -54
- 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 +20 -25
- 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 +22 -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/safe-area/SafeAreaUIProvider.js +65 -0
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -0
- package/lib/commonjs/safe-area/index.js +13 -0
- package/lib/commonjs/safe-area/index.js.map +1 -0
- package/lib/commonjs/theme/UIProvider.js +11 -0
- package/lib/commonjs/theme/UIProvider.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/palettes.js +73 -11
- package/lib/commonjs/theme/palettes.js.map +1 -1
- package/lib/module/atoms/Avatar.js +1 -1
- package/lib/module/atoms/Avatar.js.map +1 -1
- package/lib/module/atoms/Badge.js +10 -8
- package/lib/module/atoms/Badge.js.map +1 -1
- package/lib/module/atoms/Button.js +8 -6
- package/lib/module/atoms/Button.js.map +1 -1
- package/lib/module/atoms/Checkbox.js +4 -1
- package/lib/module/atoms/Checkbox.js.map +1 -1
- package/lib/module/atoms/Fab.js +1 -1
- package/lib/module/atoms/Fab.js.map +1 -1
- package/lib/module/atoms/Image.js +2 -2
- package/lib/module/atoms/Image.js.map +1 -1
- package/lib/module/atoms/Input.js +2 -2
- package/lib/module/atoms/Input.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/AvatarHeader.js +2 -2
- package/lib/module/molecules/AvatarHeader.js.map +1 -1
- package/lib/module/molecules/OptionCard.js +58 -25
- package/lib/module/molecules/OptionCard.js.map +1 -1
- package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
- 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 +2 -2
- 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/Alert.js +6 -5
- package/lib/module/organisms/Alert.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 +2 -2
- package/lib/module/organisms/DateField.js.map +1 -1
- package/lib/module/organisms/DatePicker.js +3 -3
- 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 +200 -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 +100 -31
- package/lib/module/organisms/SearchList.js.map +1 -1
- package/lib/module/organisms/Select.js +228 -56
- 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 +22 -27
- 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 +23 -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/safe-area/SafeAreaUIProvider.js +60 -0
- package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -0
- package/lib/module/safe-area/index.js +4 -0
- package/lib/module/safe-area/index.js.map +1 -0
- package/lib/module/theme/UIProvider.js +11 -0
- package/lib/module/theme/UIProvider.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/palettes.js +73 -11
- package/lib/module/theme/palettes.js.map +1 -1
- package/lib/typescript/atoms/Badge.d.ts.map +1 -1
- package/lib/typescript/atoms/Button.d.ts.map +1 -1
- package/lib/typescript/atoms/Checkbox.d.ts.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/index.d.ts +4 -4
- package/lib/typescript/molecules/index.d.ts.map +1 -1
- package/lib/typescript/organisms/Alert.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/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/safe-area/SafeAreaUIProvider.d.ts +46 -0
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -0
- package/lib/typescript/safe-area/index.d.ts +3 -0
- package/lib/typescript/safe-area/index.d.ts.map +1 -0
- package/lib/typescript/theme/UIProvider.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/palettes.d.ts +41 -3
- 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 +101 -18
- package/registry/alert-provider.json +9 -2
- package/registry/alert.json +1 -1
- package/registry/avatar-header.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/bottom-sheet.json +6 -2
- package/registry/button.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/core.json +9 -5
- package/registry/date-field.json +2 -1
- package/registry/date-picker.json +1 -1
- package/registry/fab.json +1 -1
- package/registry/image.json +1 -1
- package/registry/index.json +108 -39
- package/registry/input.json +1 -1
- 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/Avatar.tsx +1 -1
- package/src/atoms/Badge.tsx +6 -4
- package/src/atoms/Button.tsx +8 -6
- package/src/atoms/Checkbox.tsx +4 -1
- package/src/atoms/Fab.tsx +1 -1
- package/src/atoms/Image.tsx +2 -2
- package/src/atoms/Input.tsx +2 -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/AvatarHeader.tsx +2 -2
- package/src/molecules/OptionCard.tsx +64 -27
- package/src/molecules/SegmentedControl.tsx +84 -0
- package/src/molecules/Stepper.tsx +147 -32
- package/src/molecules/Steps.tsx +2 -2
- package/src/molecules/index.ts +4 -4
- package/src/organisms/Alert.tsx +6 -5
- package/src/organisms/AlertProvider.tsx +26 -1
- package/src/organisms/BottomSheet.tsx +214 -62
- package/src/organisms/DateField.tsx +2 -2
- package/src/organisms/DatePicker.tsx +3 -3
- package/src/organisms/List.tsx +73 -41
- package/src/organisms/Modal.tsx +49 -61
- package/src/organisms/NavigationBar.tsx +223 -0
- package/src/organisms/Screen.tsx +11 -3
- package/src/organisms/SearchList.tsx +134 -38
- package/src/organisms/Select.tsx +227 -49
- package/src/organisms/ThemeToggle.tsx +30 -0
- package/src/organisms/Toast.tsx +19 -24
- package/src/organisms/ToastProvider.tsx +24 -0
- package/src/organisms/Wizard.tsx +36 -3
- package/src/organisms/imperativeBridge.ts +28 -0
- package/src/organisms/index.ts +7 -3
- package/src/safe-area/SafeAreaUIProvider.tsx +67 -0
- package/src/safe-area/index.ts +2 -0
- package/src/theme/UIProvider.tsx +15 -0
- package/src/theme/colors.ts +175 -0
- package/src/theme/createTheme.ts +38 -4
- package/src/theme/index.ts +3 -1
- package/src/theme/palettes.ts +108 -11
- 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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
{
|
|
14
14
|
"path": "organisms/DatePicker.tsx",
|
|
15
|
-
"content": "import React, { memo, useMemo, useState } from 'react';\nimport { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { IconButton } from '../atoms/IconButton';\n\nexport const DEFAULT_MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Índice 0 = domingo, para alinear con Date.getDay(). */\nexport const DEFAULT_WEEKDAY_NAMES = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];\n\nexport interface DateRange {\n start: Date;\n /** Falta mientras el rango está a medio elegir (sólo se tocó el inicio). */\n end?: Date;\n}\n\nexport interface DatePickerProps {\n /**\n * 'single' elige una fecha; 'range' elige un intervalo con dos toques.\n * @default 'single'\n */\n mode?: 'single' | 'range';\n /** Fecha elegida en mode=\"single\". */\n value?: Date;\n /** Se llama en mode=\"single\". */\n onChange?: (date: Date) => void;\n /** Rango elegido en mode=\"range\". */\n range?: DateRange;\n /** Se llama en mode=\"range\", tanto al fijar el inicio como al cerrar el rango. */\n onRangeChange?: (range: DateRange) => void;\n minDate?: Date;\n maxDate?: Date;\n /** Mes mostrado al abrir si no hay `value`. @default hoy */\n defaultMonth?: Date;\n monthNames?: string[];\n weekdayNames?: string[];\n /** 0 = domingo, 1 = lunes. @default 0 */\n firstDayOfWeek?: 0 | 1;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\n/** Compara sólo año/mes/día, ignorando hora — dos Date del mismo día son \"iguales\". */\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nfunction startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n}\n\n/** Estrictamente entre start y end, sin incluir los extremos. */\nfunction isBetween(day: Date, start: Date, end: Date): boolean {\n const t = startOfDay(day).getTime();\n return t > startOfDay(start).getTime() && t < startOfDay(end).getTime();\n}\n\nfunction isOutOfRange(day: Date, minDate?: Date, maxDate?: Date): boolean {\n const t = startOfDay(day).getTime();\n if (minDate && t < startOfDay(minDate).getTime()) return true;\n if (maxDate && t > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Devuelve la grilla del mes: 6 filas de 7 celdas. Las celdas fuera del mes\n * son null, así el layout no salta de alto entre meses (un mes puede\n * necesitar 4, 5 o 6 filas según en qué día caiga el 1).\n */\nexport function buildMonthGrid(year: number, month: number, firstDayOfWeek: 0 | 1): (Date | null)[][] {\n const first = new Date(year, month, 1);\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n const leading = (first.getDay() - firstDayOfWeek + 7) % 7;\n\n const cells: (Date | null)[] = [];\n for (let i = 0; i < leading; i++) cells.push(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));\n while (cells.length < 42) cells.push(null);\n\n const rows: (Date | null)[][] = [];\n for (let r = 0; r < 6; r++) rows.push(cells.slice(r * 7, r * 7 + 7));\n return rows;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: {\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n padding: theme.tokens.spacing.md,\n },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.tokens.spacing.sm },\n monthLabel: { fontSize: theme.tokens.fontSize.lg, fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.text },\n weekRow: { flexDirection: 'row' },\n weekday: {\n flex: 1,\n textAlign: 'center',\n fontSize: theme.tokens.fontSize.xs,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.textLight,\n paddingVertical: theme.tokens.spacing.sm,\n },\n row: { flexDirection: 'row' },\n cell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },\n dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },\n dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },\n daySelected: { backgroundColor: theme.colors.primary },\n dayTextSelected: { color: theme.colors.white, fontWeight: theme.tokens.fontWeight.bold },\n // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y\n // no en el botón redondo, así la barra del rango se ve continua.\n cellInRange: { backgroundColor: theme.colors.primary + '1f' },\n cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },\n cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },\n dayTextInRange: { color: theme.colors.primary, fontWeight: theme.tokens.fontWeight.semibold },\n dayToday: { borderWidth: 1, borderColor: theme.colors.primary },\n dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },\n}));\n\n/**\n * DatePicker\n * Calendario mensual dibujado con View/Text puros — sin\n * @react-native-community/datetimepicker ni ningún picker nativo, para\n * mantener la librería en cero dependencias y verse igual en iOS y Android.\n */\nexport const DatePicker = memo(\n ({\n mode = 'single',\n value,\n onChange,\n range,\n onRangeChange,\n minDate,\n maxDate,\n defaultMonth,\n monthNames = DEFAULT_MONTH_NAMES,\n weekdayNames = DEFAULT_WEEKDAY_NAMES,\n firstDayOfWeek = 0,\n style,\n testID,\n }: DatePickerProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const isRange = mode === 'range';\n\n const handlePress = (day: Date) => {\n if (!isRange) {\n onChange?.(day);\n return;\n }\n // Primer toque (o rango ya cerrado, o toque antes del inicio): arranca\n // un rango nuevo. Segundo toque en adelante: lo cierra.\n if (!range?.start || range.end || startOfDay(day) < startOfDay(range.start)) {\n onRangeChange?.({ start: day });\n } else {\n onRangeChange?.({ start: range.start, end: day });\n }\n };\n\n const initial = value ?? range?.start ?? defaultMonth ?? new Date();\n const [visibleMonth, setVisibleMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));\n\n const today = useMemo(() => new Date(), []);\n const grid = useMemo(\n () => buildMonthGrid(visibleMonth.getFullYear(), visibleMonth.getMonth(), firstDayOfWeek),\n [visibleMonth, firstDayOfWeek]\n );\n\n const orderedWeekdays = useMemo(\n () => (firstDayOfWeek === 0 ? weekdayNames : [...weekdayNames.slice(1), weekdayNames[0]!]),\n [weekdayNames, firstDayOfWeek]\n );\n\n const shiftMonth = (delta: number) =>\n setVisibleMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));\n\n const monthLabel = `${monthNames[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;\n\n return (\n <View style={[styles.container, style]} testID={testID}>\n <View style={styles.header}>\n <IconButton iconName=\"chevron-left\" accessibilityLabel=\"Previous month\" onPress={() => shiftMonth(-1)} />\n <Text allowFontScaling={allowFontScaling} style={styles.monthLabel} accessibilityRole=\"header\">\n {monthLabel}\n </Text>\n <IconButton iconName=\"chevron-right\" accessibilityLabel=\"Next month\" onPress={() => shiftMonth(1)} />\n </View>\n\n <View style={styles.weekRow}>\n {orderedWeekdays.map((w, i) => (\n <Text allowFontScaling={allowFontScaling} key={`${w}-${i}`} style={styles.weekday}>\n {w}\n </Text>\n ))}\n </View>\n\n {grid.map((row, rowIndex) => (\n <View style={styles.row} key={rowIndex}>\n {row.map((day, cellIndex) => {\n if (!day) return <View style={styles.cell} key={cellIndex} />;\n\n const isStart = isRange && !!range?.start && isSameDay(day, range.start);\n const isEnd = isRange && !!range?.end && isSameDay(day, range.end);\n const inRange =\n isRange && !!range?.start && !!range.end && isBetween(day, range.start, range.end);\n const selected = isRange ? isStart || isEnd : !!value && isSameDay(day, value);\n\n const disabled = isOutOfRange(day, minDate, maxDate);\n const isToday = isSameDay(day, today);\n return (\n <View\n style={[\n styles.cell,\n (inRange || (isStart && !!range?.end) || isEnd) && styles.cellInRange,\n isStart && !!range?.end && styles.cellRangeStart,\n isEnd && styles.cellRangeEnd,\n ]}\n key={cellIndex}\n >\n <TouchableOpacity\n style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}\n onPress={() => handlePress(day)}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityLabel={`${monthNames[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}\n accessibilityState={{ selected, disabled }}\n >\n <Text\n allowFontScaling={allowFontScaling}\n style={[\n styles.dayText,\n inRange && styles.dayTextInRange,\n selected && styles.dayTextSelected,\n disabled && styles.dayTextDisabled,\n ]}\n >\n {day.getDate()}\n </Text>\n </TouchableOpacity>\n </View>\n );\n })}\n </View>\n ))}\n </View>\n );\n }\n);\nDatePicker.displayName = 'DatePicker';\n"
|
|
15
|
+
"content": "import React, { memo, useMemo, useState } from 'react';\nimport { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { IconButton } from '../atoms/IconButton';\n\nexport const DEFAULT_MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Índice 0 = domingo, para alinear con Date.getDay(). */\nexport const DEFAULT_WEEKDAY_NAMES = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];\n\nexport interface DateRange {\n start: Date;\n /** Falta mientras el rango está a medio elegir (sólo se tocó el inicio). */\n end?: Date;\n}\n\nexport interface DatePickerProps {\n /**\n * 'single' elige una fecha; 'range' elige un intervalo con dos toques.\n * @default 'single'\n */\n mode?: 'single' | 'range';\n /** Fecha elegida en mode=\"single\". */\n value?: Date;\n /** Se llama en mode=\"single\". */\n onChange?: (date: Date) => void;\n /** Rango elegido en mode=\"range\". */\n range?: DateRange;\n /** Se llama en mode=\"range\", tanto al fijar el inicio como al cerrar el rango. */\n onRangeChange?: (range: DateRange) => void;\n minDate?: Date;\n maxDate?: Date;\n /** Mes mostrado al abrir si no hay `value`. @default hoy */\n defaultMonth?: Date;\n monthNames?: string[];\n weekdayNames?: string[];\n /** 0 = domingo, 1 = lunes. @default 0 */\n firstDayOfWeek?: 0 | 1;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\n/** Compara sólo año/mes/día, ignorando hora — dos Date del mismo día son \"iguales\". */\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nfunction startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n}\n\n/** Estrictamente entre start y end, sin incluir los extremos. */\nfunction isBetween(day: Date, start: Date, end: Date): boolean {\n const t = startOfDay(day).getTime();\n return t > startOfDay(start).getTime() && t < startOfDay(end).getTime();\n}\n\nfunction isOutOfRange(day: Date, minDate?: Date, maxDate?: Date): boolean {\n const t = startOfDay(day).getTime();\n if (minDate && t < startOfDay(minDate).getTime()) return true;\n if (maxDate && t > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Devuelve la grilla del mes: 6 filas de 7 celdas. Las celdas fuera del mes\n * son null, así el layout no salta de alto entre meses (un mes puede\n * necesitar 4, 5 o 6 filas según en qué día caiga el 1).\n */\nexport function buildMonthGrid(year: number, month: number, firstDayOfWeek: 0 | 1): (Date | null)[][] {\n const first = new Date(year, month, 1);\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n const leading = (first.getDay() - firstDayOfWeek + 7) % 7;\n\n const cells: (Date | null)[] = [];\n for (let i = 0; i < leading; i++) cells.push(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));\n while (cells.length < 42) cells.push(null);\n\n const rows: (Date | null)[][] = [];\n for (let r = 0; r < 6; r++) rows.push(cells.slice(r * 7, r * 7 + 7));\n return rows;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: {\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n padding: theme.tokens.spacing.md,\n },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.tokens.spacing.sm },\n monthLabel: { fontSize: theme.tokens.fontSize.lg, fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.text },\n weekRow: { flexDirection: 'row' },\n weekday: {\n flex: 1,\n textAlign: 'center',\n fontSize: theme.tokens.fontSize.xs,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.textLight,\n paddingVertical: theme.tokens.spacing.sm,\n },\n row: { flexDirection: 'row' },\n cell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },\n dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },\n dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },\n daySelected: { backgroundColor: theme.colors.primary },\n dayTextSelected: { color: theme.colors.onPrimary, fontWeight: theme.tokens.fontWeight.bold },\n // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y\n // no en el botón redondo, así la barra del rango se ve continua.\n cellInRange: { backgroundColor: theme.colors.primarySoft },\n cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },\n cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },\n dayTextInRange: { color: theme.colors.primaryText, fontWeight: theme.tokens.fontWeight.semibold },\n dayToday: { borderWidth: 1, borderColor: theme.colors.primary },\n dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },\n}));\n\n/**\n * DatePicker\n * Calendario mensual dibujado con View/Text puros — sin\n * @react-native-community/datetimepicker ni ningún picker nativo, para\n * mantener la librería en cero dependencias y verse igual en iOS y Android.\n */\nexport const DatePicker = memo(\n ({\n mode = 'single',\n value,\n onChange,\n range,\n onRangeChange,\n minDate,\n maxDate,\n defaultMonth,\n monthNames = DEFAULT_MONTH_NAMES,\n weekdayNames = DEFAULT_WEEKDAY_NAMES,\n firstDayOfWeek = 0,\n style,\n testID,\n }: DatePickerProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const isRange = mode === 'range';\n\n const handlePress = (day: Date) => {\n if (!isRange) {\n onChange?.(day);\n return;\n }\n // Primer toque (o rango ya cerrado, o toque antes del inicio): arranca\n // un rango nuevo. Segundo toque en adelante: lo cierra.\n if (!range?.start || range.end || startOfDay(day) < startOfDay(range.start)) {\n onRangeChange?.({ start: day });\n } else {\n onRangeChange?.({ start: range.start, end: day });\n }\n };\n\n const initial = value ?? range?.start ?? defaultMonth ?? new Date();\n const [visibleMonth, setVisibleMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));\n\n const today = useMemo(() => new Date(), []);\n const grid = useMemo(\n () => buildMonthGrid(visibleMonth.getFullYear(), visibleMonth.getMonth(), firstDayOfWeek),\n [visibleMonth, firstDayOfWeek]\n );\n\n const orderedWeekdays = useMemo(\n () => (firstDayOfWeek === 0 ? weekdayNames : [...weekdayNames.slice(1), weekdayNames[0]!]),\n [weekdayNames, firstDayOfWeek]\n );\n\n const shiftMonth = (delta: number) =>\n setVisibleMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));\n\n const monthLabel = `${monthNames[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;\n\n return (\n <View style={[styles.container, style]} testID={testID}>\n <View style={styles.header}>\n <IconButton iconName=\"chevron-left\" accessibilityLabel=\"Previous month\" onPress={() => shiftMonth(-1)} />\n <Text allowFontScaling={allowFontScaling} style={styles.monthLabel} accessibilityRole=\"header\">\n {monthLabel}\n </Text>\n <IconButton iconName=\"chevron-right\" accessibilityLabel=\"Next month\" onPress={() => shiftMonth(1)} />\n </View>\n\n <View style={styles.weekRow}>\n {orderedWeekdays.map((w, i) => (\n <Text allowFontScaling={allowFontScaling} key={`${w}-${i}`} style={styles.weekday}>\n {w}\n </Text>\n ))}\n </View>\n\n {grid.map((row, rowIndex) => (\n <View style={styles.row} key={rowIndex}>\n {row.map((day, cellIndex) => {\n if (!day) return <View style={styles.cell} key={cellIndex} />;\n\n const isStart = isRange && !!range?.start && isSameDay(day, range.start);\n const isEnd = isRange && !!range?.end && isSameDay(day, range.end);\n const inRange =\n isRange && !!range?.start && !!range.end && isBetween(day, range.start, range.end);\n const selected = isRange ? isStart || isEnd : !!value && isSameDay(day, value);\n\n const disabled = isOutOfRange(day, minDate, maxDate);\n const isToday = isSameDay(day, today);\n return (\n <View\n style={[\n styles.cell,\n (inRange || (isStart && !!range?.end) || isEnd) && styles.cellInRange,\n isStart && !!range?.end && styles.cellRangeStart,\n isEnd && styles.cellRangeEnd,\n ]}\n key={cellIndex}\n >\n <TouchableOpacity\n style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}\n onPress={() => handlePress(day)}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityLabel={`${monthNames[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}\n accessibilityState={{ selected, disabled }}\n >\n <Text\n allowFontScaling={allowFontScaling}\n style={[\n styles.dayText,\n inRange && styles.dayTextInRange,\n selected && styles.dayTextSelected,\n disabled && styles.dayTextDisabled,\n ]}\n >\n {day.getDate()}\n </Text>\n </TouchableOpacity>\n </View>\n );\n })}\n </View>\n ))}\n </View>\n );\n }\n);\nDatePicker.displayName = 'DatePicker';\n"
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
18
|
"registryDependencies": [
|
package/registry/fab.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "atoms/Fab.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport { type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';\nimport { PressableScale } from './PressableScale';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors } from '../theme/UIProvider';\n\nexport interface FabProps {\n onPress: (e: GestureResponderEvent) => void;\n iconName?: IconName;\n icon?: React.ReactNode;\n size?: number;\n bottom?: number;\n right?: number;\n color?: string;\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n fab: {\n position: 'absolute',\n justifyContent: 'center',\n alignItems: 'center',\n ...theme.tokens.shadow.lg,\n },\n}));\n\n/**\n * Fab\n * Botón flotante circular. `bottom`/`right` en vez del `tabBarHeight`\n * obligatorio de la versión original — el consumidor decide su propio\n * offset (con o sin tab bar).\n */\nexport const Fab = memo(\n ({ onPress, iconName = 'plus', icon, size = 60, bottom = 20, right = 20, color, style, accessibilityLabel }: FabProps) => {\n const styles = useStyles();\n const colors = useColors();\n return (\n <PressableScale\n onPress={onPress}\n style={[\n styles.fab,\n {\n width: size,\n height: size,\n borderRadius: size / 2,\n bottom,\n right,\n backgroundColor: color ?? colors.primary,\n },\n style,\n ]}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n >\n {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';\nimport { PressableScale } from './PressableScale';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors } from '../theme/UIProvider';\n\nexport interface FabProps {\n onPress: (e: GestureResponderEvent) => void;\n iconName?: IconName;\n icon?: React.ReactNode;\n size?: number;\n bottom?: number;\n right?: number;\n color?: string;\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n fab: {\n position: 'absolute',\n justifyContent: 'center',\n alignItems: 'center',\n ...theme.tokens.shadow.lg,\n },\n}));\n\n/**\n * Fab\n * Botón flotante circular. `bottom`/`right` en vez del `tabBarHeight`\n * obligatorio de la versión original — el consumidor decide su propio\n * offset (con o sin tab bar).\n */\nexport const Fab = memo(\n ({ onPress, iconName = 'plus', icon, size = 60, bottom = 20, right = 20, color, style, accessibilityLabel }: FabProps) => {\n const styles = useStyles();\n const colors = useColors();\n return (\n <PressableScale\n onPress={onPress}\n style={[\n styles.fab,\n {\n width: size,\n height: size,\n borderRadius: size / 2,\n bottom,\n right,\n backgroundColor: color ?? colors.primary,\n },\n style,\n ]}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n >\n {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.onPrimary} />}\n </PressableScale>\n );\n }\n);\nFab.displayName = 'Fab';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
package/registry/image.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"path": "atoms/Image.tsx",
|
|
13
|
-
"content": "import React, { memo, useCallback, useEffect, useState } from 'react';\nimport {\n Image as RNImage,\n Platform,\n StyleSheet,\n View,\n type ImageErrorEventData,\n type ImageResizeMode,\n type ImageSourcePropType,\n type NativeSyntheticEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Spinner } from './Spinner';\n\nexport interface ImageProps {\n source: ImageSourcePropType;\n width?: number;\n height?: number;\n /** @default 'cover' */\n resizeMode?: ImageResizeMode;\n /** Radio de las esquinas, mismo patrón que Avatar/Card. @default 0 */\n radius?: number;\n /** Qué mostrar mientras carga. `false` para no mostrar nada. @default true (spinner chico) */\n loading?: React.ReactNode | boolean;\n /** Qué mostrar si la carga falla. @default un placeholder gris, sin contenido */\n fallback?: React.ReactNode;\n /**\n * 'high' dispara Image.prefetch() al montar, antes de que <Image> arranque\n * su propio ciclo de carga — pensado para la imagen above-the-fold de una\n * pantalla (la primera que el usuario ve). Sin costo si ya está en caché.\n * @default 'normal'\n */\n priority?: 'low' | 'normal' | 'high';\n style?: StyleProp<ViewStyle>;\n onLoadEnd?: () => void;\n onError?: (error: NativeSyntheticEvent<ImageErrorEventData>) => void;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { overflow: 'hidden', backgroundColor: theme.colors.
|
|
13
|
+
"content": "import React, { memo, useCallback, useEffect, useState } from 'react';\nimport {\n Image as RNImage,\n Platform,\n StyleSheet,\n View,\n type ImageErrorEventData,\n type ImageResizeMode,\n type ImageSourcePropType,\n type NativeSyntheticEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Spinner } from './Spinner';\n\nexport interface ImageProps {\n source: ImageSourcePropType;\n width?: number;\n height?: number;\n /** @default 'cover' */\n resizeMode?: ImageResizeMode;\n /** Radio de las esquinas, mismo patrón que Avatar/Card. @default 0 */\n radius?: number;\n /** Qué mostrar mientras carga. `false` para no mostrar nada. @default true (spinner chico) */\n loading?: React.ReactNode | boolean;\n /** Qué mostrar si la carga falla. @default un placeholder gris, sin contenido */\n fallback?: React.ReactNode;\n /**\n * 'high' dispara Image.prefetch() al montar, antes de que <Image> arranque\n * su propio ciclo de carga — pensado para la imagen above-the-fold de una\n * pantalla (la primera que el usuario ve). Sin costo si ya está en caché.\n * @default 'normal'\n */\n priority?: 'low' | 'normal' | 'high';\n style?: StyleProp<ViewStyle>;\n onLoadEnd?: () => void;\n onError?: (error: NativeSyntheticEvent<ImageErrorEventData>) => void;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { overflow: 'hidden', backgroundColor: theme.colors.skeleton },\n overlay: { alignItems: 'center', justifyContent: 'center' },\n fallback: { flex: 1, backgroundColor: theme.colors.skeleton },\n}));\n\nfunction sourceUri(source: ImageSourcePropType): string | undefined {\n return typeof source === 'object' && source !== null && 'uri' in source ? source.uri : undefined;\n}\n\n/**\n * Image\n * Wrapper cero-dependencias sobre <Image> de RN: agrega estado de carga y\n * error (con los mismos placeholders que el resto de la librería), y expone\n * `priority=\"high\"` para adelantar la descarga vía Image.prefetch(). El\n * cache real (disco/memoria) lo maneja el motor de imágenes nativo de RN —\n * esto no lo reimplementa, solo no lo deja apagado por accidente: en iOS\n * fuerza `cache=\"force-cache\"` (usa el disco aunque el header HTTP no lo\n * pida); en Android el cache de disco de la imagen nativa ya está siempre\n * activo, no hay knob equivalente que tocar.\n */\nexport const Image = memo(\n ({\n source,\n width,\n height,\n resizeMode = 'cover',\n radius = 0,\n loading = true,\n fallback,\n priority = 'normal',\n style,\n onLoadEnd,\n onError,\n testID,\n }: ImageProps) => {\n const styles = useStyles();\n const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');\n const uri = sourceUri(source);\n\n useEffect(() => {\n // Resetea el estado si cambia la imagen (misma instancia de Image,\n // source distinto: p.ej. avatar de un usuario a otro en la misma fila).\n setStatus('loading');\n }, [uri ?? source]);\n\n useEffect(() => {\n if (priority === 'high' && uri) {\n RNImage.prefetch(uri).catch(() => {});\n }\n }, [priority, uri]);\n\n const handleError = useCallback(\n (e: NativeSyntheticEvent<ImageErrorEventData>) => {\n setStatus('error');\n onError?.(e);\n },\n [onError]\n );\n\n const handleLoadEnd = useCallback(() => {\n setStatus('loaded');\n onLoadEnd?.();\n }, [onLoadEnd]);\n\n return (\n <View style={[styles.container, { width, height, borderRadius: radius }, style]} testID={testID}>\n {status !== 'error' && (\n <RNImage\n source={source}\n resizeMode={resizeMode}\n style={StyleSheet.absoluteFill}\n fadeDuration={Platform.OS === 'android' ? 150 : undefined}\n {...(Platform.OS === 'ios' ? { cache: 'force-cache' as const } : null)}\n onLoadEnd={handleLoadEnd}\n onError={handleError}\n />\n )}\n {status === 'loading' && loading !== false && (\n <View style={[StyleSheet.absoluteFill, styles.overlay]}>\n {loading === true ? <Spinner size=\"small\" fullscreen={false} /> : loading}\n </View>\n )}\n {status === 'error' && (fallback ?? <View style={styles.fallback} />)}\n </View>\n );\n }\n);\nImage.displayName = 'Image';\n\n/**\n * Precarga una URL antes de montar el componente (p.ej. al recibir la\n * respuesta de un listado, antes de navegar al detalle). Re-exporta\n * Image.prefetch de RN — no hay nada zero-dep que agregarle encima.\n */\nexport const prefetchImage = RNImage.prefetch;\n"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"registryDependencies": [
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"core": {
|
|
3
|
-
"bytes":
|
|
3
|
+
"bytes": 35122
|
|
4
4
|
},
|
|
5
5
|
"components": [
|
|
6
6
|
{
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"registryDependencies": [
|
|
29
29
|
"core"
|
|
30
30
|
],
|
|
31
|
-
"bytes":
|
|
31
|
+
"bytes": 6790
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"name": "IconButton",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"registryDependencies": [
|
|
54
54
|
"core"
|
|
55
55
|
],
|
|
56
|
-
"bytes":
|
|
56
|
+
"bytes": 5447
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"name": "Checkbox",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"registryDependencies": [
|
|
66
66
|
"core"
|
|
67
67
|
],
|
|
68
|
-
"bytes":
|
|
68
|
+
"bytes": 2847
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"name": "Badge",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"registryDependencies": [
|
|
78
78
|
"core"
|
|
79
79
|
],
|
|
80
|
-
"bytes":
|
|
80
|
+
"bytes": 2172
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "Card",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"registryDependencies": [
|
|
114
114
|
"core"
|
|
115
115
|
],
|
|
116
|
-
"bytes":
|
|
116
|
+
"bytes": 1012
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"name": "Image",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"core",
|
|
128
128
|
"spinner"
|
|
129
129
|
],
|
|
130
|
-
"bytes":
|
|
130
|
+
"bytes": 4610
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"name": "Spinner",
|
|
@@ -141,6 +141,31 @@
|
|
|
141
141
|
],
|
|
142
142
|
"bytes": 4626
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
"name": "Skeleton",
|
|
146
|
+
"slug": "skeleton",
|
|
147
|
+
"category": "atoms",
|
|
148
|
+
"exportedNames": [
|
|
149
|
+
"Skeleton"
|
|
150
|
+
],
|
|
151
|
+
"registryDependencies": [
|
|
152
|
+
"core"
|
|
153
|
+
],
|
|
154
|
+
"bytes": 3554
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "Transition",
|
|
158
|
+
"slug": "transition",
|
|
159
|
+
"category": "atoms",
|
|
160
|
+
"exportedNames": [
|
|
161
|
+
"Transition",
|
|
162
|
+
"useReduceMotion"
|
|
163
|
+
],
|
|
164
|
+
"registryDependencies": [
|
|
165
|
+
"core"
|
|
166
|
+
],
|
|
167
|
+
"bytes": 6558
|
|
168
|
+
},
|
|
144
169
|
{
|
|
145
170
|
"name": "EmptyState",
|
|
146
171
|
"slug": "empty-state",
|
|
@@ -176,7 +201,7 @@
|
|
|
176
201
|
"core",
|
|
177
202
|
"pressable-scale"
|
|
178
203
|
],
|
|
179
|
-
"bytes":
|
|
204
|
+
"bytes": 1788
|
|
180
205
|
},
|
|
181
206
|
{
|
|
182
207
|
"name": "PressableScale",
|
|
@@ -188,19 +213,20 @@
|
|
|
188
213
|
"registryDependencies": [
|
|
189
214
|
"core"
|
|
190
215
|
],
|
|
191
|
-
"bytes":
|
|
216
|
+
"bytes": 1709
|
|
192
217
|
},
|
|
193
218
|
{
|
|
194
219
|
"name": "Stepper",
|
|
195
220
|
"slug": "stepper",
|
|
196
221
|
"category": "molecules",
|
|
197
222
|
"exportedNames": [
|
|
198
|
-
"Stepper"
|
|
223
|
+
"Stepper",
|
|
224
|
+
"sanitizeNumeric"
|
|
199
225
|
],
|
|
200
226
|
"registryDependencies": [
|
|
201
227
|
"core"
|
|
202
228
|
],
|
|
203
|
-
"bytes":
|
|
229
|
+
"bytes": 6717
|
|
204
230
|
},
|
|
205
231
|
{
|
|
206
232
|
"name": "OptionCard",
|
|
@@ -210,9 +236,10 @@
|
|
|
210
236
|
"OptionCard"
|
|
211
237
|
],
|
|
212
238
|
"registryDependencies": [
|
|
213
|
-
"core"
|
|
239
|
+
"core",
|
|
240
|
+
"pressable-scale"
|
|
214
241
|
],
|
|
215
|
-
"bytes":
|
|
242
|
+
"bytes": 4156
|
|
216
243
|
},
|
|
217
244
|
{
|
|
218
245
|
"name": "InfoRow",
|
|
@@ -250,19 +277,19 @@
|
|
|
250
277
|
"registryDependencies": [
|
|
251
278
|
"core"
|
|
252
279
|
],
|
|
253
|
-
"bytes":
|
|
280
|
+
"bytes": 2487
|
|
254
281
|
},
|
|
255
282
|
{
|
|
256
|
-
"name": "
|
|
257
|
-
"slug": "
|
|
283
|
+
"name": "SegmentedControl",
|
|
284
|
+
"slug": "segmented-control",
|
|
258
285
|
"category": "molecules",
|
|
259
286
|
"exportedNames": [
|
|
260
|
-
"
|
|
287
|
+
"SegmentedControl"
|
|
261
288
|
],
|
|
262
289
|
"registryDependencies": [
|
|
263
290
|
"core"
|
|
264
291
|
],
|
|
265
|
-
"bytes":
|
|
292
|
+
"bytes": 2884
|
|
266
293
|
},
|
|
267
294
|
{
|
|
268
295
|
"name": "Steps",
|
|
@@ -274,7 +301,7 @@
|
|
|
274
301
|
"registryDependencies": [
|
|
275
302
|
"core"
|
|
276
303
|
],
|
|
277
|
-
"bytes":
|
|
304
|
+
"bytes": 7314
|
|
278
305
|
},
|
|
279
306
|
{
|
|
280
307
|
"name": "Modal",
|
|
@@ -287,9 +314,10 @@
|
|
|
287
314
|
"core",
|
|
288
315
|
"title",
|
|
289
316
|
"icon-button",
|
|
290
|
-
"pressable-scale"
|
|
317
|
+
"pressable-scale",
|
|
318
|
+
"transition"
|
|
291
319
|
],
|
|
292
|
-
"bytes":
|
|
320
|
+
"bytes": 8647
|
|
293
321
|
},
|
|
294
322
|
{
|
|
295
323
|
"name": "BottomSheet",
|
|
@@ -299,9 +327,13 @@
|
|
|
299
327
|
"BottomSheet"
|
|
300
328
|
],
|
|
301
329
|
"registryDependencies": [
|
|
302
|
-
"core"
|
|
330
|
+
"core",
|
|
331
|
+
"title",
|
|
332
|
+
"icon-button",
|
|
333
|
+
"pressable-scale",
|
|
334
|
+
"transition"
|
|
303
335
|
],
|
|
304
|
-
"bytes":
|
|
336
|
+
"bytes": 8662
|
|
305
337
|
},
|
|
306
338
|
{
|
|
307
339
|
"name": "Select",
|
|
@@ -313,7 +345,7 @@
|
|
|
313
345
|
"registryDependencies": [
|
|
314
346
|
"core"
|
|
315
347
|
],
|
|
316
|
-
"bytes":
|
|
348
|
+
"bytes": 12122
|
|
317
349
|
},
|
|
318
350
|
{
|
|
319
351
|
"name": "Alert",
|
|
@@ -326,7 +358,7 @@
|
|
|
326
358
|
"core",
|
|
327
359
|
"button"
|
|
328
360
|
],
|
|
329
|
-
"bytes":
|
|
361
|
+
"bytes": 6240
|
|
330
362
|
},
|
|
331
363
|
{
|
|
332
364
|
"name": "AlertProvider",
|
|
@@ -334,14 +366,17 @@
|
|
|
334
366
|
"category": "organisms",
|
|
335
367
|
"exportedNames": [
|
|
336
368
|
"AlertProvider",
|
|
337
|
-
"useAlert"
|
|
369
|
+
"useAlert",
|
|
370
|
+
"showAlert",
|
|
371
|
+
"showConfirm",
|
|
372
|
+
"hideAlert"
|
|
338
373
|
],
|
|
339
374
|
"registryDependencies": [
|
|
340
375
|
"core",
|
|
341
376
|
"alert",
|
|
342
377
|
"button"
|
|
343
378
|
],
|
|
344
|
-
"bytes":
|
|
379
|
+
"bytes": 5470
|
|
345
380
|
},
|
|
346
381
|
{
|
|
347
382
|
"name": "Screen",
|
|
@@ -353,7 +388,7 @@
|
|
|
353
388
|
"registryDependencies": [
|
|
354
389
|
"core"
|
|
355
390
|
],
|
|
356
|
-
"bytes":
|
|
391
|
+
"bytes": 3321
|
|
357
392
|
},
|
|
358
393
|
{
|
|
359
394
|
"name": "List",
|
|
@@ -365,9 +400,10 @@
|
|
|
365
400
|
"registryDependencies": [
|
|
366
401
|
"core",
|
|
367
402
|
"empty-state",
|
|
403
|
+
"skeleton",
|
|
368
404
|
"spinner"
|
|
369
405
|
],
|
|
370
|
-
"bytes":
|
|
406
|
+
"bytes": 6039
|
|
371
407
|
},
|
|
372
408
|
{
|
|
373
409
|
"name": "SearchList",
|
|
@@ -380,11 +416,12 @@
|
|
|
380
416
|
"core",
|
|
381
417
|
"input",
|
|
382
418
|
"empty-state",
|
|
419
|
+
"skeleton",
|
|
383
420
|
"spinner",
|
|
384
421
|
"icon-button",
|
|
385
422
|
"pressable-scale"
|
|
386
423
|
],
|
|
387
|
-
"bytes":
|
|
424
|
+
"bytes": 9327
|
|
388
425
|
},
|
|
389
426
|
{
|
|
390
427
|
"name": "Toast",
|
|
@@ -394,9 +431,10 @@
|
|
|
394
431
|
"Toast"
|
|
395
432
|
],
|
|
396
433
|
"registryDependencies": [
|
|
397
|
-
"core"
|
|
434
|
+
"core",
|
|
435
|
+
"transition"
|
|
398
436
|
],
|
|
399
|
-
"bytes":
|
|
437
|
+
"bytes": 4402
|
|
400
438
|
},
|
|
401
439
|
{
|
|
402
440
|
"name": "ToastProvider",
|
|
@@ -404,13 +442,17 @@
|
|
|
404
442
|
"category": "organisms",
|
|
405
443
|
"exportedNames": [
|
|
406
444
|
"ToastProvider",
|
|
407
|
-
"useToast"
|
|
445
|
+
"useToast",
|
|
446
|
+
"showToast",
|
|
447
|
+
"hideToast",
|
|
448
|
+
"hideAllToasts"
|
|
408
449
|
],
|
|
409
450
|
"registryDependencies": [
|
|
410
451
|
"core",
|
|
411
|
-
"toast"
|
|
452
|
+
"toast",
|
|
453
|
+
"transition"
|
|
412
454
|
],
|
|
413
|
-
"bytes":
|
|
455
|
+
"bytes": 6368
|
|
414
456
|
},
|
|
415
457
|
{
|
|
416
458
|
"name": "DatePicker",
|
|
@@ -427,7 +469,7 @@
|
|
|
427
469
|
"icon-button",
|
|
428
470
|
"pressable-scale"
|
|
429
471
|
],
|
|
430
|
-
"bytes":
|
|
472
|
+
"bytes": 10098
|
|
431
473
|
},
|
|
432
474
|
{
|
|
433
475
|
"name": "DateField",
|
|
@@ -442,10 +484,11 @@
|
|
|
442
484
|
"title",
|
|
443
485
|
"icon-button",
|
|
444
486
|
"pressable-scale",
|
|
487
|
+
"transition",
|
|
445
488
|
"date-picker",
|
|
446
489
|
"button"
|
|
447
490
|
],
|
|
448
|
-
"bytes":
|
|
491
|
+
"bytes": 4553
|
|
449
492
|
},
|
|
450
493
|
{
|
|
451
494
|
"name": "Wizard",
|
|
@@ -457,9 +500,35 @@
|
|
|
457
500
|
"registryDependencies": [
|
|
458
501
|
"core",
|
|
459
502
|
"steps",
|
|
460
|
-
"button"
|
|
503
|
+
"button",
|
|
504
|
+
"transition"
|
|
505
|
+
],
|
|
506
|
+
"bytes": 5591
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"name": "ThemeToggle",
|
|
510
|
+
"slug": "theme-toggle",
|
|
511
|
+
"category": "organisms",
|
|
512
|
+
"exportedNames": [
|
|
513
|
+
"ThemeToggle"
|
|
514
|
+
],
|
|
515
|
+
"registryDependencies": [
|
|
516
|
+
"core",
|
|
517
|
+
"segmented-control"
|
|
518
|
+
],
|
|
519
|
+
"bytes": 1189
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"name": "NavigationBar",
|
|
523
|
+
"slug": "navigation-bar",
|
|
524
|
+
"category": "organisms",
|
|
525
|
+
"exportedNames": [
|
|
526
|
+
"NavigationBar"
|
|
527
|
+
],
|
|
528
|
+
"registryDependencies": [
|
|
529
|
+
"core"
|
|
461
530
|
],
|
|
462
|
-
"bytes":
|
|
531
|
+
"bytes": 7378
|
|
463
532
|
}
|
|
464
533
|
]
|
|
465
534
|
}
|
package/registry/input.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "atoms/Input.tsx",
|
|
12
|
-
"content": "import React, { memo, useRef, useState } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n type StyleProp,\n type TextInputProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface InputProps extends Omit<TextInputProps, 'style'> {\n label?: string;\n /** Muestra un asterisco junto al label. Puramente visual, no valida nada. */\n required?: boolean;\n error?: string;\n containerStyle?: StyleProp<ViewStyle>;\n style?: TextInputProps['style'];\n leftIconName?: IconName;\n rightIconName?: IconName;\n onRightIconPress?: () => void;\n isPassword?: boolean;\n isLoading?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { marginBottom: theme.tokens.spacing.lg, width: '100%' },\n label: {\n marginBottom: theme.tokens.spacing.sm,\n fontSize: theme.tokens.fontSize.md,\n fontWeight: theme.tokens.fontWeight.medium,\n color: theme.colors.text,\n },\n required: { color: theme.colors.
|
|
12
|
+
"content": "import React, { memo, useRef, useState } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n type StyleProp,\n type TextInputProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface InputProps extends Omit<TextInputProps, 'style'> {\n label?: string;\n /** Muestra un asterisco junto al label. Puramente visual, no valida nada. */\n required?: boolean;\n error?: string;\n containerStyle?: StyleProp<ViewStyle>;\n style?: TextInputProps['style'];\n leftIconName?: IconName;\n rightIconName?: IconName;\n onRightIconPress?: () => void;\n isPassword?: boolean;\n isLoading?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { marginBottom: theme.tokens.spacing.lg, width: '100%' },\n label: {\n marginBottom: theme.tokens.spacing.sm,\n fontSize: theme.tokens.fontSize.md,\n fontWeight: theme.tokens.fontWeight.medium,\n color: theme.colors.text,\n },\n required: { color: theme.colors.errorText },\n inputWrapper: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: theme.colors.inputBackground,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 2,\n minHeight: 50,\n },\n input: {\n flex: 1,\n paddingVertical: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.lg,\n fontSize: theme.tokens.fontSize.lg,\n color: theme.colors.text,\n },\n leftIcon: { marginLeft: theme.tokens.spacing.lg, marginRight: -theme.tokens.spacing.xs },\n rightIcon: { padding: theme.tokens.spacing.sm + 2, marginRight: theme.tokens.spacing.xs },\n errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },\n}));\n\n/**\n * Input\n * El borde anima color entre border/primary/error con Animated (no\n * useNativeDriver: los navegadores de color en RN sin Fabric solo son\n * confiables en el hilo de JS; el costo es despreciable, se anima un borde).\n */\nexport const Input = memo(\n ({\n label,\n required = false,\n error,\n containerStyle,\n leftIconName,\n rightIconName,\n onRightIconPress,\n isPassword = false,\n isLoading = false,\n style,\n onFocus,\n onBlur,\n testID,\n ...props\n }: InputProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const [isFocused, setIsFocused] = useState(false);\n const [showPassword, setShowPassword] = useState(false);\n const focusProgress = useRef(new Animated.Value(0)).current;\n\n const handleFocus: TextInputProps['onFocus'] = (e) => {\n setIsFocused(true);\n Animated.timing(focusProgress, { toValue: 1, duration: 200, useNativeDriver: false }).start();\n onFocus?.(e);\n };\n\n const handleBlur: TextInputProps['onBlur'] = (e) => {\n setIsFocused(false);\n Animated.timing(focusProgress, { toValue: 0, duration: 200, useNativeDriver: false }).start();\n onBlur?.(e);\n };\n\n const borderColor = error\n ? colors.error\n : focusProgress.interpolate({ outputRange: [colors.border, colors.primary], inputRange: [0, 1] });\n\n return (\n <View style={[styles.container, containerStyle]} testID={testID}>\n {label && (\n <Text allowFontScaling={allowFontScaling} style={styles.label}>\n {label}\n {required && <Text style={styles.required}> *</Text>}\n </Text>\n )}\n\n <Animated.View style={[styles.inputWrapper, { borderColor }]}>\n {leftIconName && <Icon name={leftIconName} size={20} color={colors.textLight} style={styles.leftIcon} />}\n\n <TextInput\n style={[styles.input, style]}\n placeholderTextColor={colors.textLight}\n onFocus={handleFocus}\n onBlur={handleBlur}\n secureTextEntry={isPassword && !showPassword}\n allowFontScaling={allowFontScaling}\n textAlignVertical=\"center\"\n accessibilityLabel={label ?? props.placeholder}\n {...props}\n />\n\n {isLoading ? (\n <View style={styles.rightIcon}>\n <ActivityIndicator size=\"small\" color={colors.primary} />\n </View>\n ) : isPassword ? (\n <TouchableOpacity\n onPress={() => setShowPassword((v) => !v)}\n style={styles.rightIcon}\n accessibilityRole=\"button\"\n accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}\n >\n <Icon name={showPassword ? 'eye-off' : 'eye'} size={20} color={colors.textLight} />\n </TouchableOpacity>\n ) : rightIconName ? (\n <TouchableOpacity\n onPress={onRightIconPress}\n style={styles.rightIcon}\n disabled={!onRightIconPress}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Clear\"\n >\n <Icon name={rightIconName} size={20} color={colors.textLight} />\n </TouchableOpacity>\n ) : null}\n </Animated.View>\n\n {!!error && (\n <Text allowFontScaling={allowFontScaling} style={styles.errorText}>\n {error}\n </Text>\n )}\n </View>\n );\n }\n);\nInput.displayName = 'Input';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
package/registry/list.json
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/List.tsx",
|
|
12
|
-
"content": "import React, {
|
|
12
|
+
"content": "import React, { useCallback } from 'react';\nimport { FlatList, RefreshControl, View, type FlatListProps, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useLabels } from '../theme/UIProvider';\nimport { EmptyState } from '../atoms/EmptyState';\nimport { Skeleton } from '../atoms/Skeleton';\nimport { Spinner } from '../atoms/Spinner';\nimport type { IconName } from '../icons/types';\n\nexport interface ListProps<T> {\n data: T[];\n keyExtractor: (item: T, index: number) => string;\n renderItem: FlatListProps<T>['renderItem'];\n\n /** Carga inicial: mientras sea true y no haya datos, se muestran placeholders. */\n isLoading: boolean;\n /** Recarga con datos ya en pantalla (pull-to-refresh). @default false */\n isRefreshing?: boolean;\n isLoadingMore?: boolean;\n /** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */\n isReady?: boolean;\n loadingText?: string;\n loadingMoreText?: string;\n\n /** Filas fantasma de la carga inicial. @default 6 */\n skeletonCount?: number;\n /** Placeholder propio, para que calce con la forma real del ítem. */\n renderSkeletonItem?: () => React.ReactElement;\n\n onEndReached?: () => void;\n onEndReachedThreshold?: number;\n onRefresh?: () => void;\n\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIconName?: IconName;\n\n ListHeaderComponent?: FlatListProps<T>['ListHeaderComponent'];\n ListFooterComponent?: FlatListProps<T>['ListFooterComponent'];\n\n contentContainerStyle?: ViewStyle;\n containerStyle?: ViewStyle;\n\n /** Componente de lista a usar (FlashList, etc). @default FlatList */\n ListComponent?: React.ComponentType<any>;\n listProps?: Record<string, unknown>;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, backgroundColor: theme.colors.background },\n listContent: { padding: theme.tokens.spacing.md },\n footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },\n skeletonRow: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n padding: theme.tokens.spacing.md,\n },\n skeletonText: { flex: 1, gap: theme.tokens.spacing.sm },\n}));\n\n/**\n * List\n * Lista con estados de carga/paginación/vacío resueltos, sin atarse a\n * expo-router ni a FlashList: `ListComponent` permite inyectar cualquiera\n * (por defecto FlatList del propio React Native).\n */\nexport function List<T>({\n data,\n keyExtractor,\n renderItem,\n isLoading,\n isRefreshing = false,\n isLoadingMore = false,\n isReady = true,\n loadingText,\n loadingMoreText,\n skeletonCount = 6,\n renderSkeletonItem,\n onEndReached,\n onEndReachedThreshold = 0.3,\n onRefresh,\n emptyTitle,\n emptyDescription,\n emptyIconName,\n ListHeaderComponent,\n ListFooterComponent,\n contentContainerStyle,\n containerStyle,\n ListComponent = FlatList,\n listProps,\n}: ListProps<T>) {\n const colors = useColors();\n const styles = useStyles();\n const labels = useLabels();\n const resolvedLoadingText = loadingText ?? labels.loading;\n const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;\n\n const isInitialLoading = isLoading && data.length === 0;\n\n // El footer propio y el loader de paginación conviven: antes el primero\n // tapaba al segundo y la lista dejaba de avisar que estaba trayendo más.\n const renderFooter = useCallback(() => {\n const custom =\n typeof ListFooterComponent === 'function' ? (\n <ListFooterComponent />\n ) : (\n (ListFooterComponent as React.ReactElement | undefined) ?? null\n );\n\n return (\n <>\n {custom}\n {isLoadingMore && (\n <View style={styles.footerLoader}>\n <Spinner text={resolvedLoadingMoreText} fullscreen={false} />\n </View>\n )}\n </>\n );\n }, [isLoadingMore, resolvedLoadingMoreText, ListFooterComponent, styles.footerLoader]);\n\n // Vacío sólo cuando ya se sabe que no hay nada: durante la carga inicial se\n // muestran placeholders, no \"sin resultados\".\n const renderEmpty = useCallback(() => {\n if (isLoading || isRefreshing) return null;\n return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;\n }, [isLoading, isRefreshing, emptyTitle, emptyDescription, emptyIconName]);\n\n if (!isReady) {\n return (\n <View style={styles.container}>\n <Spinner text={resolvedLoadingText} />\n </View>\n );\n }\n\n if (isInitialLoading) {\n return (\n <View style={[styles.container, containerStyle]} testID=\"list-skeleton\">\n <View style={[styles.listContent, contentContainerStyle]}>\n {Array.from({ length: skeletonCount }, (_, i) => (\n <React.Fragment key={i}>\n {renderSkeletonItem?.() ?? (\n <View style={styles.skeletonRow}>\n <Skeleton variant=\"circle\" width={40} />\n <View style={styles.skeletonText}>\n <Skeleton width=\"60%\" height={14} />\n <Skeleton width=\"35%\" height={12} />\n </View>\n </View>\n )}\n </React.Fragment>\n ))}\n </View>\n </View>\n );\n }\n\n return (\n <View style={[styles.container, containerStyle]}>\n <ListComponent\n data={data}\n keyExtractor={keyExtractor}\n renderItem={renderItem}\n contentContainerStyle={[styles.listContent, contentContainerStyle]}\n onEndReached={onEndReached}\n onEndReachedThreshold={onEndReachedThreshold}\n ListHeaderComponent={ListHeaderComponent}\n ListFooterComponent={renderFooter}\n ListEmptyComponent={renderEmpty}\n refreshControl={\n onRefresh ? (\n <RefreshControl\n refreshing={isRefreshing}\n onRefresh={onRefresh}\n colors={[colors.primary]}\n tintColor={colors.primary}\n />\n ) : undefined\n }\n {...listProps}\n />\n </View>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
|
16
16
|
"core",
|
|
17
17
|
"empty-state",
|
|
18
|
+
"skeleton",
|
|
18
19
|
"spinner"
|
|
19
20
|
]
|
|
20
21
|
}
|
package/registry/manifest.json
CHANGED
|
@@ -83,6 +83,22 @@
|
|
|
83
83
|
"file": "atoms/Spinner.tsx",
|
|
84
84
|
"slug": "spinner"
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
"name": "Skeleton",
|
|
88
|
+
"siblingNames": [],
|
|
89
|
+
"category": "atoms",
|
|
90
|
+
"file": "atoms/Skeleton.tsx",
|
|
91
|
+
"slug": "skeleton"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "Transition",
|
|
95
|
+
"siblingNames": [
|
|
96
|
+
"useReduceMotion"
|
|
97
|
+
],
|
|
98
|
+
"category": "atoms",
|
|
99
|
+
"file": "atoms/Transition.tsx",
|
|
100
|
+
"slug": "transition"
|
|
101
|
+
},
|
|
86
102
|
{
|
|
87
103
|
"name": "EmptyState",
|
|
88
104
|
"siblingNames": [],
|
|
@@ -113,7 +129,9 @@
|
|
|
113
129
|
},
|
|
114
130
|
{
|
|
115
131
|
"name": "Stepper",
|
|
116
|
-
"siblingNames": [
|
|
132
|
+
"siblingNames": [
|
|
133
|
+
"sanitizeNumeric"
|
|
134
|
+
],
|
|
117
135
|
"category": "molecules",
|
|
118
136
|
"file": "molecules/Stepper.tsx",
|
|
119
137
|
"slug": "stepper"
|
|
@@ -147,11 +165,11 @@
|
|
|
147
165
|
"slug": "avatar-header"
|
|
148
166
|
},
|
|
149
167
|
{
|
|
150
|
-
"name": "
|
|
168
|
+
"name": "SegmentedControl",
|
|
151
169
|
"siblingNames": [],
|
|
152
170
|
"category": "molecules",
|
|
153
|
-
"file": "molecules/
|
|
154
|
-
"slug": "
|
|
171
|
+
"file": "molecules/SegmentedControl.tsx",
|
|
172
|
+
"slug": "segmented-control"
|
|
155
173
|
},
|
|
156
174
|
{
|
|
157
175
|
"name": "Steps",
|
|
@@ -191,7 +209,10 @@
|
|
|
191
209
|
{
|
|
192
210
|
"name": "AlertProvider",
|
|
193
211
|
"siblingNames": [
|
|
194
|
-
"useAlert"
|
|
212
|
+
"useAlert",
|
|
213
|
+
"showAlert",
|
|
214
|
+
"showConfirm",
|
|
215
|
+
"hideAlert"
|
|
195
216
|
],
|
|
196
217
|
"category": "organisms",
|
|
197
218
|
"file": "organisms/AlertProvider.tsx",
|
|
@@ -228,7 +249,10 @@
|
|
|
228
249
|
{
|
|
229
250
|
"name": "ToastProvider",
|
|
230
251
|
"siblingNames": [
|
|
231
|
-
"useToast"
|
|
252
|
+
"useToast",
|
|
253
|
+
"showToast",
|
|
254
|
+
"hideToast",
|
|
255
|
+
"hideAllToasts"
|
|
232
256
|
],
|
|
233
257
|
"category": "organisms",
|
|
234
258
|
"file": "organisms/ToastProvider.tsx",
|
|
@@ -258,6 +282,20 @@
|
|
|
258
282
|
"category": "organisms",
|
|
259
283
|
"file": "organisms/Wizard.tsx",
|
|
260
284
|
"slug": "wizard"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "ThemeToggle",
|
|
288
|
+
"siblingNames": [],
|
|
289
|
+
"category": "organisms",
|
|
290
|
+
"file": "organisms/ThemeToggle.tsx",
|
|
291
|
+
"slug": "theme-toggle"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"name": "NavigationBar",
|
|
295
|
+
"siblingNames": [],
|
|
296
|
+
"category": "organisms",
|
|
297
|
+
"file": "organisms/NavigationBar.tsx",
|
|
298
|
+
"slug": "navigation-bar"
|
|
261
299
|
}
|
|
262
300
|
]
|
|
263
301
|
}
|