Saltar a contenido

Documento histórico

Este documento se conserva solo para trazabilidad. Consulte el índice maestro para la fuente vigente.

Componentes UI

Documento histórico — catálogo de componentes

Este archivo es el catálogo de componentes de la APK. Para conocer el estado actual vigente de los componentes y su uso, consultar estado-actual.md.

Catálogo exhaustivo de los 31 componentes reutilizables definidos en la APK, agrupados por carpeta.


Resumen

Carpeta Cantidad Propósito
ui/components/common/ 6 archivos, ~15 funciones Base: botones, cards, text fields, fondos, logos, imágenes
ui/components/auth/ 1 archivo, 1 función Top bar de autenticación
ui/components/dashboard/ 6 archivos, 6 funciones Componentes del dashboard y bottom bar
ui/components/reporte/ 9 archivos, 9 funciones Componentes de reportes y paquetes
ui/components/envios/ 6 archivos, ~6 funciones Componentes de envíos (HUÉRFANOS — no se usan)
ui/components/perfil/ 3 archivos, 3 funciones Componentes del perfil
ui/components/guia/ 3 archivos, 3 funciones Componentes del centro de ayuda

1. ui/components/common/

IslaButtons.kt

Funciones exportadas:

Función Firma Variante visual
IslaPrimaryButton (text, onClick, modifier, containerColor=Primary, contentColor=OnPrimary, enabled=true, shape=small) Botón filled, full-width, altura mínima 48dp
IslaSecondaryButton (text, onClick, modifier, containerColor=SurfaceContainerHighest, contentColor=OnSurface, enabled, shape) Filled con color neutro
IslaInvertedButton (text, onClick, modifier, containerColor=InverseSurface, contentColor=InverseOnSurface, enabled, shape) Atajo: delega en IslaPrimaryButton con colores invertidos
IslaOutlinedButton (text, onClick, modifier, containerColor=White, contentColor=Primary, borderColor=Primary, enabled, shape) Outlined con borde 1.5dp

Detalles comunes: - heightIn(min = 48dp) (estándar accesibilidad touch target) - Texto con bodyLarge, FontWeight.SemiBold, letterSpacing 0.5.sp - Padding LocalSpacing.current.md horizontal y vertical - disabledContainerColor = outlineVariant

Usado en: Welcome, Login, Register, ForgotPassword, Success, ReporteForm, ReporteScreen, ReportesScreen.

IslaCards.kt

Funciones exportadas:

Función Firma Detalle
IslaCard (modifier, onClick: (() -> Unit)?, content: @Composable ColumnScope.() -> Unit) Card base, esquinas 20dp, borde 1dp gris claro, sin elevación. Variante con onClick usa Card(onClick=...)
IslaInfoCard (modifier, title: String?, content) Envuelve IslaCard con padding interno + título opcional uppercase

Usado en: ReporteCard, ReportePrealertaItem, PackageCard, ReporteDetalleScreen, StorageInfoCard, PerfilScreen, LockerAddressSection, ReportActionBanner, FeaturedGuideCard, GuideListItem, ProfileHeader, PackageSummarySection.

IslaTextFields.kt

Funciones exportadas:

Función Firma Detalle
IslaTextField (value, onValueChange, label, modifier, placeholder?, leadingIcon?, leadingContent?, trailingIcon?, visualTransformation, keyboardOptions, keyboardActions, singleLine, readOnly, error?) OutlinedTextField con label uppercase, asterisco rojo automático para campos requeridos, error visible
IslaPhoneField (phone, onPhoneChange, selectedCountry: Country, onCountrySelected, modifier, error?) TextField con selector de país (leading content). Usado para el User.phone (registro/KYC), donde solo aplica +1. Para destinatarios en Cuba se creará IslaPhoneFieldInternational (ver T-209).
data class Country (name, code, flag, maxLength) Modelo de país
val countries Lista Solo 1 país: Estados Unidos +1, max 10 dígitos — correcto por diseño (ver Regla de origen del usuario)
Detalle de campos "requeridos" (asterisco automático):
if (!readOnly && label.contains(Regex("Tienda|Orden|Tracking|Fecha|Categoría|Descripción|Cantidad|Valor|Destinatario", RegexOption.IGNORE_CASE))) {
    Text(" *", color = error, ...)
}

Es una validación basada en el texto del label. Cualquier label que contenga "Tienda", "Orden", "Tracking", "Fecha", "Categoría", "Descripción", "Cantidad", "Valor" o "Destinatario" muestra asterisco.

Limitación de IslaPhoneField (resuelta por diseño): hoy solo permite 10 dígitos para US. Esto es correcto para el campo User.phone porque el usuario que se registra vive en EE.UU. (ver la Regla de origen del usuario). Para soportar teléfonos de destinatarios en Cuba (+53) se creará el componente IslaPhoneFieldInternational (ver T-209 en tareas.md). La lista de países admitidos para destinatarios se decide en ../../../roadmap/preguntas.md. La lista de países admitidos para destinatarios se decide en ../../../roadmap/preguntas.md.

Usado en: LoginScreen, RegisterScreen, ForgotPasswordScreen, ReporteForm, DestinatarioSelector (interno), RemitenteSelector (interno).


2. ui/components/auth/

IslaAuthTopBar.kt

Catálogo continúa en la siguiente sección.

Funciones exportadas:

Función Firma Detalle
DotGridBackground (modifier, color=outline.copy(alpha=0.3f)) Fondo con cuadrícula técnica animada: líneas discontinuas, círculos de 3/4 decorativos, animación de entrada (2.5s EaseOutQuart)

Detalles técnicos: - gapPx = 64.dp - PathEffect.dashPathEffect(floatArrayOf(10f, 15f), 0f) para líneas discontinuas - Animación: animateFloatAsState(targetValue = if (startAnim) 1f else 0f, animationSpec = tween(2500, easing = EaseOutQuart)) - Solo se anima una vez al primer render (LaunchedEffect(Unit) { startAnim = true })

Usado en: Welcome, Login, Register, ForgotPassword, VerificationCode, Success, Notifications (como fondo alternativo).

IslaLogo.kt

Funciones exportadas:

Función Firma Detalle
IslaLogo (modifier, height=48.dp) Imagen con IslaImages.Logo (es el isotipo de palmera dentro de círculo)
IslaHorizontalLogo (modifier, fontSize=24.sp, iconSize=32.dp) Palmera + texto "islabox" (palmera desde imagen, texto en Compose)
IslaTextLogo (modifier, fontSize=24.sp) Solo texto "isla" (navy) + "box" (rojo), construido con AnnotatedString

Usado en: IslaHorizontalLogo y IslaTextLogo están definidos pero no se usan en ninguna pantalla. Solo IslaLogo se referencia, y este depende de IslaImages.Logo que no existe.

IslaImages.kt

Objeto centralizado para acceder a drawables:

object IslaImages {
    val Logo: Painter     @Composable get() = painterResource(R.drawable.ic_islabox_logo)      // ❌ NO EXISTE
    val Logo2: Painter    @Composable get() = painterResource(R.drawable.ic_islabox_logo2)     // ❌ NO EXISTE
    val Letra: Painter    @Composable get() = painterResource(R.drawable.ic_islabox_letra)     // ❌ NO EXISTE
    val Success: Painter  @Composable get() = painterResource(R.drawable.ic_islabox_success)   // ❌ NO EXISTE
    val Placeholder: Painter @Composable get() = painterResource(android.R.drawable.ic_menu_report_image)  // ✅ fallback
}

Usado en: - WelcomeScreenIslaImages.Logo (CRASH) - SuccessScreenIslaImages.Success (CRASH) - IslaDashboardTopBarIslaImages.Letra (CRASH visual) - IslaTopBar (auth) → IslaImages.Letra (CRASH visual)


2. ui/components/auth/

IslaTopBar.kt

@Composable
fun IslaTopBar(
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier,
    showLogo: Boolean = true
)

Detalle: Box con IconButton de back a la izquierda y IslaImages.Letra centrada si showLogo. Padding horizontal 8dp, vertical 4dp.

Usado en: Login, Register, ForgotPassword, VerificationCode. El logo no se renderiza porque el drawable no existe (cae a Placeholder solo si se cambia manualmente).


3. ui/components/dashboard/

IslaDashboardTopBar.kt

@Composable
fun IslaDashboardTopBar(onNotificationsClick: () -> Unit = {})

Detalle: - TopAppBar transparente - Title: IslaImages.Letra (logo, 50dp alto) → CRASH porque el drawable no existe - Action: Surface(40dp, CircleShape, White, border) con Icon(Notifications) + punto rojo decorativo (Color 0xFFE91E63, 8dp)

Usado en: MainLayout (todas las pantallas del bottom nav).

IslaBottomBar.kt

@Composable
fun IslaBottomBar(navController: NavController)

Detalle: - Box con padding(horizontal=24dp, vertical=24dp) y navigationBarsPadding() - Surface píldora (RoundedCornerShape 100dp) de 72dp alto, blanco con shadowElevation=8dp - 5 items distribuidos equitativamente - Burbuja móvil circular de 52dp con animateDpAsState (spring damping 0.8, stiffness MediumLow) - Iconos: iconScale 1.0 → 1.2 cuando seleccionado, iconAlpha 0.45 → 1.0

Cálculo del índice actual:

val selectedIndex = BottomNavItem.items.indexOfFirst { item ->
    currentDestination?.hasRoute(item.route::class) == true
}

Manejo del índice -1 (cuando no hay match, por ejemplo en ReporteForm):

var lastValidIndex by remember { mutableIntStateOf(0) }
SideEffect {
    if (selectedIndex != -1 && selectedIndex != lastValidIndex) {
        lastValidIndex = selectedIndex
    }
}

Usado en: MainLayout (todas las pantallas del bottom nav).

PackageSummarySection.kt

data class PackageSummary(
    val inTransit: Int = 0,
    val inWarehouse: Int = 0,
    val delivered: Int = 0
)

@Composable
fun PackageSummarySection(lockerId: String, summary: PackageSummary, modifier: Modifier = Modifier)

Detalle: - Fila superior: punto verde (8dp) + texto "Identificador Casillero {lockerId}" - 3 StatCards en Row: En Tránsito (rojo), En Almacén (gris), Entregados (verde) - Cada StatCard: dot color + label + count en headlineMedium bold

Usado en: DashboardScreen con summary = PackageSummary(2, 3, 12) hardcoded.

LockerAddressSection.kt

@Composable
fun LockerAddressSection(
    firstName, lastName, lockerId,
    addressLine1, addressLine2, city, state, zipCode, phone,
    onCopyLine: (String) -> Unit = {}
)

Detalle: - Selector segmentado Amazon/Shein/Temu (3 botones en Row con Surface elevado para el seleccionado) - Card con: - Header: ícono circular 44dp + título "Tu Dirección en EE. UU." + subtítulo + badge "MIA" (rojo) - Divider 0.5dp - Banner rojo (Color 0xFFBB0016 alpha 0.08) con Info: "Asegúrate de incluir la Línea 2; es fundamental para identificar tu paquete." - 3 bloques de campos según plataforma seleccionada (when sobre selectedPlatform): - "Amazon": campos con addressLine1, displayLine2 (concatena), etc. - "Shein": layout adaptado - "Temu": layout adaptado - Cada campo es un Row con label + value + IconButton de copy

Concatenación de Line 2:

val displayLine2 = if (addressLine2.isNotEmpty()) "$addressLine2 $lockerId" else lockerId
// Resultado: "ISLABOX CP-001042-7"

⚠️ La doc dice que en Address Line 2 va solo el identificador. La app concatena con la marca.

Usado en: DashboardScreen con valores hardcoded del casillero Miami.

GreetingSection.kt

@Composable
fun GreetingSection(userName: String, modifier: Modifier = Modifier)

Detalle: - "Bienvenido," en bodyLarge - userName en headlineMedium Primary, Bold

Usado en: DashboardScreen con userName = "Kleudy Victor".

ReportActionBanner.kt

@Composable
fun ReportActionBanner(onClick: () -> Unit, modifier: Modifier = Modifier)

Detalle: Card con ícono circular 44dp + "¿Compraste algo nuevo?" + "Repórtalo ahora para agilizar el envío" + botón redondo 36dp navy con flecha ArrowForwardIos.

Usado en: DashboardScreen (navega a Screen.Dashboard.Reportes).


4. ui/components/reporte/

ReporteForm.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReporteForm(
    tienda, onTiendaChange,
    orden, onOrdenChange,
    tracking, onTrackingChange,
    fecha, onFechaChange,
    categoria, onCategoriaChange,
    descripcion, onDescripcionChange,
    cantidad, onCantidadChange,
    valor, onValorChange,
    enlace, onEnlaceChange,
    instrucciones, onInstruccionesChange,
    deseaConsolidar: Boolean?, onConsolidarChange: (Boolean) -> Unit,
    serviciosSeleccionados: List<String>, onServicioToggle: (String) -> Unit
)

Detalle: - 3 secciones con label uppercase Primary: - "INFORMACIÓN OBLIGATORIA" (Tienda, Orden+Fecha, Tracking, Categoría dropdown, Descripción+Cantidad, Valor) - "LOGÍSTICA" (Surface con "Sí/No" para consolidar) - "INFORMACIÓN OPCIONAL" (Enlace, Instrucciones, Servicios adicionales con Checkbox) - Dropdown de Categoría: ExposedDropdownMenuBox con menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) - Categorías del dropdown vienen de CATEGORIAS_PRODUCTO (8 items) - Servicios adicionales vienen de SERVICIOS_ADICIONALES (3 items)

Usado en: ReporteScreen (ModalBottomSheet) y FormularioReporteScreen (full screen).

ReporteCard.kt

@Composable
fun ReporteCard(reporte: Reporte, onClick: () -> Unit, modifier: Modifier = Modifier)

Detalle: - Tienda (titleMedium Black) + Categoría (badge pill navy) - InfoRows: TRACKING, ORDEN (si existe), RECIBIDO (si existe), PESO+DIM (si existen), ESTADO (si existe) - Footer: "Para: {destinatario}" + fechaCompra

Usado en: ReporteScreen (la pantalla huérfana).

ReportePrealertaItem.kt

@Composable
fun ReportePrealertaItem(reporte: Reporte, onClick: () -> Unit, modifier: Modifier = Modifier)

Detalle: - Ícono de paquete 72dp en Box 0xFFF1F4F9 (si no hay foto) - Tienda (titleMedium Black) + Estado (badge pill navy) - "Tracking: {numeroTracking}" o "Sin tracking aún" (si vacío) - 2 columnas: ORDEN + FECHA COMPRA

Usado en: ReportesScreen.

PackageCard.kt

@Composable
fun PackageCard(paquete: Reporte, onClick: () -> Unit, modifier: Modifier = Modifier)

Detalle: - Foto del paquete (Coil AsyncImage si fotos no está vacío, si no ícono) - Tienda + Estado (badge pill) - "Tracking: {numeroTracking}" - 3 columnas: RECIBIDO / DÍAS ALMACÉN (rojo si > 5) / FECHA LÍMITE

Usado en: PaquetesScreen.

PackageTimeline.kt

@Composable
fun PackageTimeline(steps: List<StatusStep>, modifier: Modifier = Modifier)

Detalle: - Timeline vertical, IntrinsicSize.Min - Cada step: línea vertical (2dp) + dot (12dp) + texto (status + date - time) - Filtra steps con status no vacío

Usado en: ReporteDetalleScreen (con default si reporte.timeline está vacío).

StorageInfoCard.kt

@Composable
fun StorageInfoCard(paquete: Reporte, modifier: Modifier = Modifier)

Detalle: - 3 filas con HorizontalDivider 0.3f alpha entre ellas: - Fecha recepción / Días en almacén - "15 días" hardcoded (días gratis, según preguntas.md) / "$0.50 USD" hardcoded (costo por día extra, según misma decisión) - "Total acumulado" + "$${costoAlmacenaje} USD" (rojo si > 0)

Usado en: ReporteDetalleScreen.

CaptureUploadSection.kt

@Composable
fun CaptureUploadSection(hasImage: Boolean, onUploadClick: () -> Unit)

Detalle: - Card surfaceVariant con ícono CloudUpload 40dp + texto - Si hasImage muestra "Cambiar captura de pantalla", si no "Subir captura de confirmación (Opcional)"

Usado en: ReporteScreen y FormularioReporteScreen.

RemitenteSelector.kt

@Composable
fun RemitenteSelector(userName: String, modifier: Modifier = Modifier)

Detalle: - Label "REMITENTE (EE.UU.)" - Card con ícono PersonOutline + nombre + "Remitente por defecto" - No permite cambiar de remitente (la doc dice que un usuario puede tener múltiples)

Usado en: ReporteScreen y FormularioReporteScreen con userName = "Kleudy Victor".

DestinatarioSelector.kt

@Composable
fun DestinatarioSelector(
    selectedDestinatario: String?,
    onSelectClick: () -> Unit,
    modifier: Modifier = Modifier
)

Detalle: - Label "DESTINATARIO EN CUBA" con asterisco (requerido) - Si selectedDestinatario == null: Box con border + ícono Add + "Seleccionar contacto" - Si seleccionado: Card navy alpha 0.1 con ícono Person + texto

Usado en: ReporteScreen y FormularioReporteScreen.


5. ui/components/envios/ — ⚠️ COMPONENTES HUÉRFANOS

Ninguno de estos componentes se usa en ninguna pantalla de la navegación actual. Fueron pensados para una pantalla "Envíos" que nunca se creó.

EnvioCard.kt

@Composable
fun EnvioCard(
    trackingId, status, estimatedDelivery, platform,
    shippingType?, weight?, daysInWarehouse?,
    isSelectable=false, isSelected=false, onSelectChange={}
)

Detalle: - Header: ícono del logo + plataforma (label) + trackingId + (opcional checkbox) o (ícono aéreo/marítimo + Lbs) - ShipmentTimeline(currentStatus=status) - StorageControl(daysInWarehouse) si está en almacén - Footer: STATUS (rojo o navy) + "Entrega Est.: {estimatedDelivery}"

Estado: ⚠️ HUÉRFANO.

EnviosStatusGrid.kt

@Composable
fun EnviosStatusGrid()

Detalle: - Título "Mis Envíos" - 2×2 grid de StatusCard: En Almacén (primary, count=3), Enviados (count=1), En Tránsito (count=0), Entregados (count=12)

Estado: ⚠️ HUÉRFANO.

EnviosFilter.kt

@Composable
fun EnviosFilter(
    selectedStatus: String,
    statusCounts: Map<String, Int>,
    onStatusSelected: (String) -> Unit
)

Detalle: - Fila horizontal con scroll, 6 botones: Todos, En Almacén, Enviados, En Tránsito, En Aduana, Entregados - Outlined con border, color navy si seleccionado

Estado: ⚠️ HUÉRFANO.

ShipmentTimeline.kt

enum class ShipmentStatus(val label: String, val step: Int) {
    REPORTADO("Reportado", 0),
    EN_ALMACEN("En Almacén", 1),
    CONSOLIDADO("Consolidado", 2),
    DESPACHADO("Despachado", 3),
    EN_ADUANA("En Aduana", 4),
    ENTREGADO("Entregado", 5)
}

@Composable
fun ShipmentTimeline(currentStatus: String, modifier: Modifier = Modifier)

Detalle: - Timeline horizontal con 6 estados - Calcula currentStep por label match - Cada step: líneas horizontales + dot + label (fontSize 8sp)

Estado: ⚠️ HUÉRFANO (solo se usa dentro de EnvioCard).

CostBreakdown.kt

@Composable
fun CostBreakdown(
    shippingCost: Double,
    serviceFee: Double,
    customsEstimate: Double,
    modifier: Modifier = Modifier
)

Detalle: - Label "DESGLOSE DE COSTOS" - 2 CostItem: Flete Logístico + Cargos de Servicio - Divider + Total a Pagar (IslaBox) - Surface terciaryContainer con "Estimado de Aduana (Cuba)" + "Este monto se abona al recibir en destino"

Estado: ⚠️ HUÉRFANO. Útil cuando se implemente la pantalla de cotización.

StorageControl.kt

@Composable
fun StorageControl(
    daysInWarehouse: Int,
    freeDaysLimit: Int = 30,
    modifier: Modifier = Modifier
)

Detalle: - Calcula remainingDays = (freeDaysLimit - daysInWarehouse).coerceAtLeast(0) - Color: verde si > 10, amarillo si > 3, rojo si ≤ 3 - Surface alpha 0.1 + ícono Info + texto

⚠️ Inconsistencia con StorageInfoCard.kt: este usa freeDaysLimit = 30, aquel muestra "15 días" hardcoded. Resuelto en preguntas.md: el valor correcto es 15 días gratis en toda la app.

Estado: ⚠️ HUÉRFANO.


6. ui/components/perfil/

ProfileHeader.kt

@Composable
fun ProfileHeader(name: String, email: String, lockerId: String)

Detalle: - Card con Row: - Avatar 70dp CircularShape navy con iniciales (calculadas en PerfilScreen: "KV") - Column: nombre (titleMedium Black), email (bodySmall alpha 0.6), identificador con punto verde + texto verde

Usado en: PerfilScreen con name = "Kleudy Victor Torno Peña", email = "kleudy@example.com", lockerId = "CP-001042-7".

ProfileMenuItem.kt

@Composable
fun ProfileMenuItem(
    icon: ImageVector,
    title: String,
    onClick: () -> Unit = {}
)

Detalle: - Row full-width clickable + padding md - Ícono 36dp en Surface 10dp corner con Primary alpha 0.05 - Título bodyLarge Medium Primary - ChevronRight al final

Usado en: PerfilScreen (8 items).

ProfileSectionTitle.kt

@Composable
fun ProfileSectionTitle(title: String)

Detalle: - Text labelLarge Bold Primary con padding start 8dp, bottom 8dp

Usado en: PerfilScreen (secciones "CUENTA" y "PREFERENCIAS").


7. ui/components/guia/

GuiaTitleSection.kt

@Composable
fun GuiaTitleSection(modifier: Modifier = Modifier)

Detalle: - "CENTRO DE AYUDA" labelLarge rosa 0xFFE91E63 letterSpacing 1 - "Guía" headlineLarge navy Bold - Subtítulo bodyMedium onSurfaceVariant

Usado en: GuiaScreen.

FeaturedGuideCard.kt

@Composable
fun FeaturedGuideCard(modifier: Modifier = Modifier)

Detalle: - Card con: - Badge "EMPIEZA AQUÍ" rosa con ícono AutoAwesome - Título "Tu paquete, en tres pasos." (headlineMedium) - Subtítulo descriptivo - 3 StepItem numerados (1 Compra, 2 Reporta, 3 Recibe) en Row

Usado en: GuiaScreen.

GuideListItem.kt

@Composable
fun GuideListItem(
    icon: ImageVector,
    title: String,
    subtitle: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    iconContainerColor: Color = 0xFFF1F4F9,
    iconTint: Color = navy
)

Detalle: - Card clickable con Row: - Box 48dp con color e ícono customizables - Column con título (bodyLarge Bold navy) + subtítulo - ChevronRight al final

Usado en: GuiaScreen con 6 items.


Componentes faltantes para MVP

Componente Uso Notas
ReporteListItem o ReporteCardCompact Lista de prealertas en Dashboard Hoy se redirige a ReportesScreen
ActionItem Reutilizable para "Requerir tu atención" en Dashboard Hoy no existe la sección
EmptyState Para listas vacías Se reinventa en cada pantalla con Box + Text
ErrorState Para errores de carga No existe
LoadingIndicator Para estados de carga No existe (Resource.Loading está en el modelo)
ConfirmationDialog Para confirmaciones No existe
KycStepIndicator Para los 6 pasos del KYC No existe
OtpInputField (extraído) Reutilizable Definido dentro de VerificationCodeScreen.kt, debería estar en common/
ServiceRequestItem Lista de servicios adicionales Hoy es List<String>
ConsolidationItem Lista de paquetes en consolidación No existe
NotificationItem Lista de notificaciones NotificationsScreen solo tiene empty state
RecipientPickerSheet Bottom sheet para elegir destinatario DestinatarioSelector solo setea un string
AddressPickerSheet Bottom sheet para elegir dirección del casillero No existe
PhotoCapture Cámara o galería con preview No existe (necesario para KYC, evidencias)
DocumentScanner Captura de documento con guía visual No existe
CategoryDropdown Reutilizable con metadata Hoy es ExposedDropdownMenuBox inline en ReporteForm
StatusBadge Badge reutilizable con variantes Se reinventa en cada card con Surface(shape=RoundedCornerShape(100))
ProgressBar Progreso de envío No existe
CountryDropdown (extendido) Múltiples países Hoy solo US

Tareas de componentes

# Tarea Prioridad
1 Decidir qué hacer con componentes de envios/ huérfanos Alta
2 Crear los 4 drawables faltantes para que IslaImages funcione Alta
3 Extraer OtpInputField a common/ Baja
4 Crear EmptyState y LoadingIndicator reutilizables Media
5 Unificar freeDaysLimit (5 vs 30) en StorageInfoCard y StorageControl Media
6 Mover countries a un CountryRepository o constante compartida Baja
7 Quitar hardcoded "Kleudy Victor" de RemitenteSelector (debería ser del usuario actual) Alta
8 Implementar realmente onCopyLine de LockerAddressSection (ya llama clipboardManager.setText) Hecho en DashboardScreen