UX
Patterns.
Reglas de comportamiento que viven encima de los componentes. Definen cuando usar que, en que orden, y con que copy. Un componente sin patron es una pieza suelta.
Loading.
La percepcion de velocidad es mas importante que la velocidad real. Skeleton siempre gana sobre spinner. Spinner solo cuando no hay forma de anticipar el layout.
Duracion visible: 200ms delay, luego skeleton hasta carga
Texto: Ninguno (el skeleton comunica suficiente)
Error fallback: Si falla despues de 10s → Empty state "Error al cargar" con boton "Reintentar"
Duracion visible: 200ms delay, luego skeleton
Texto: Solo si > 5s: "Cargando pedidos..."
Error fallback: Si falla → Empty state "Error al cargar" + toast "No se pudo conectar"
Copy del boton: "Confirmando..." (no "Procesando" — usar el verbo de la accion)
Exito: Toast success "Pedido PED-08431 confirmado" + redirigir a detalle
Error: Toast error "No se pudo confirmar. Intenta de nuevo." + boton vuelve a activo
Skeleton para listas, grids, tablas, cards — cualquier layout predecible. Minimo 200ms de delay antes de mostrar (evitar flash).
Spinner inline en botones durante submit. El boton se deshabilita y cambia su copy al gerundio: "Guardando...", "Eliminando...", "Confirmando..."
Nunca un spinner full-page centrado. Eso bloquea la interfaz y comunica que algo salio mal. El usuario debe poder navegar mientras carga.
Nunca mostrar loading si la carga tarda < 200ms. El flash del skeleton es peor que la espera. Usa un delay programatico.
Error handling.
Todo error tiene un tipo, un nivel de severidad y un copy canonico. El usuario nunca debe ver un error tecnico crudo. Cada error sugiere una accion concreta.
| Codigo | Tipo | Componente | Copy canonico |
|---|---|---|---|
| Validacion | Campo invalido | Inline error (debajo del campo) | "Este campo es obligatorio" / "Formato invalido" / "El RFC debe tener 12 o 13 caracteres" |
| 400 | Solicitud incorrecta | Toast error | "No se pudo completar la accion. Revisa los datos e intenta de nuevo." |
| 401 | No autenticado | Redirigir a login | "Tu sesion expiro. Inicia sesion de nuevo." |
| 403 | Sin permisos | Empty state "Sin acceso" | "No tienes permisos para ver esta seccion. Contacta al administrador." |
| 404 | No encontrado | Error page 404 | "Pagina no encontrada. La URL que buscas no existe o fue movida." |
| 408 / timeout | Timeout | Toast error + retry | "La solicitud tardo demasiado. Intenta de nuevo." |
| 429 | Rate limit | Toast warning | "Demasiadas solicitudes. Espera un momento e intenta de nuevo." |
| 500 | Error del servidor | Error page 500 o toast | "Algo salio mal de nuestro lado. Estamos trabajando en ello." |
| 503 | Mantenimiento | Error page mantenimiento | "Estamos actualizando el sistema. Regresa en aproximadamente 30 minutos." |
| Offline | Sin conexion | Empty state global + toast | "Estas sin internet. Los datos que ves pueden estar desactualizados." |
Paso 2: El usuario corrige y reenvía
Paso 3: Servidor responde 400 "Stock insuficiente para Ribeye Limousin"
Paso 4: Toast error con el mensaje del servidor + boton vuelve a activo
Prohibido: Nunca mostrar inline + toast + modal al mismo tiempo. Maximo 2 niveles simultaneos.
Todo error sugiere una accion: "Intenta de nuevo", "Revisa los datos", "Contacta al administrador", "Ir al inicio". Nunca solo describir el problema.
Errores inline validan en onBlur. Los errores del servidor se muestran como toast. Errores fatales (500, offline) reemplazan el contenido.
Nunca mostrar codigos HTTP al usuario. "Error 500" no comunica nada. Usar el copy canonico de la tabla de arriba.
Nunca usar alert() nativo del navegador. Todo error pasa por el sistema de toast o inline validation del design system.
Form validation.
Validar en onBlur, no en tiempo real. El usuario merece terminar de escribir antes de que le digamos que esta mal. Errores del servidor se manejan aparte.
Cuando el usuario sale del campo. Aplica para: email, RFC, telefono, campos con formato especifico. El error aparece inline debajo del campo.
Cuando el usuario pulsa el boton principal. Aplica para: campos requeridos vacios, validaciones cruzadas ("la fecha de entrega no puede ser anterior a hoy"). Scroll al primer error.
No validar mientras el usuario escribe. Un campo de email mostraria error desde la primera letra. Excepcion unica: contador de caracteres (textarea).
El error inline desaparece tan pronto el campo vuelve a ser valido (en onChange, solo para limpiar, no para validar). El usuario sabe que corrigio bien.
| Tipo de error | Copy canonico | Notas |
|---|---|---|
| Campo vacio | "Este campo es obligatorio." | Nunca "Required" ni "Campo requerido". |
| Email invalido | "Ingresa un email valido." | No mostrar regex ni formato esperado. |
| RFC invalido | "El RFC debe tener 12 o 13 caracteres." | Especifico de Mexico. Sin guiones. |
| Telefono invalido | "Ingresa un telefono de 10 digitos." | Formato: 222 123 4567 (ver glosario). |
| Contrasena debil | "La contrasena debe tener al menos 8 caracteres." | No pedir mayusculas + numeros + simbolos juntos. Solo longitud. |
| Fuera de rango | "El peso debe ser entre 0.1 y 999.9 kg." | Incluir los limites exactos. |
| Formato de fecha | "Usa el formato dd/mm/yyyy." | Formato mexicano, no americano. |
| Duplicado | "Ya existe un cliente con este RFC." | Error del servidor. Se muestra como inline si es un campo, o toast si es general. |
| Fecha pasada | "La fecha de entrega no puede ser anterior a hoy." | Validacion cruzada. onSubmit. |
Siempre texto + icono para el error. Nunca solo color rojo — WCAG requiere que el error sea comprensible sin color (daltonismo).
Scroll automatico al primer campo con error en onSubmit. Focus en ese campo. El usuario no tiene que buscarlo.
Nunca deshabilitar el boton de submit por campos invalidos. El usuario no sabe por que esta deshabilitado. Dejar que pulse y mostrar los errores.
Nunca "Error en el formulario" generico. Cada campo con error tiene su propio mensaje especifico debajo.
Confirmaciones.
No toda accion necesita confirmacion. Solo las destructivas o irreversibles. El exceso de modales de confirmacion entrena al usuario a pulsar "aceptar" sin leer.
Titulo: "Eliminar pedido PED-08431"
Copy: "Esta accion no se puede deshacer. Se eliminara el pedido y todos sus datos asociados."
Boton primario: "Eliminar pedido" (rojo, --signal)
Boton secundario: "Cancelar"
Post-accion: Toast success "Pedido eliminado" + redirigir a lista
Titulo: "Confirmar pedido"
Copy: Resumen: 14 cortes · 32.6 kg · Hotel Camino Real · Entrega hoy 14:00–17:00
Total destacado: $ 8,240.00 MXN
Boton primario: "Confirmar pedido" (negro, --ink)
Post-accion: Boton → "Confirmando..." → Toast success
Post-accion: Redirigir a login + toast info "Sesion cerrada"
Razon: Cerrar sesion es reversible (volver a iniciar). No necesita modal.
Search.
Busqueda unificada en App y Cloud. Un solo input, resultados agrupados por tipo. El placeholder es parte de la marca.
Placeholder: "Buscar corte, cliente o pedido..." (App) / "Buscar pedido, cliente o producto..." (Cloud). Nunca solo "Buscar" a secas.
Debounce de 300ms. No buscar en cada keystroke. Mostrar spinner inline en el input despues de 300ms si la busqueda tarda.
Agrupar por tipo: Pedidos (PED-xxxxx), Clientes (nombre), Productos (corte). Maximo 5 resultados por grupo. Link "Ver todos" al final.
Copy: "No encontramos resultados para '[query]'. Intenta con otro termino." Nunca "0 results" ni "No matches found".
Atajo de teclado: Cmd+K (Mac) / Ctrl+K (Windows) para abrir busqueda en Cloud. Indicar en el UI con badge.
Highlight del termino buscado en los resultados con <mark> y background sutil (--bone). No bold — es ruidoso.
Nunca redirigir automaticamente si hay un solo resultado. El usuario debe elegir explicitamente. Excepcion: busqueda por ID exacto (PED-08431).
Nunca paginar resultados de busqueda. Si hay mas de 5 por tipo, mostrar "Ver todos los pedidos con '[query]'" que lleva a la vista filtrada.