Patrón UX mobile: lista compacta → detalle full-screen con URL sync
Patrón UX mobile: lista compacta → detalle full-screen con URL sync
¿Qué es este patrón?
En CreaRack Pro, varias tools usan un layout split desktop (lista a la izquierda, panel de detalle a la derecha). En pantallas pequeñas ese layout colapsa en un single-column ilegible. Este patrón define cómo adaptar esas tools a mobile de forma consistente.
Origen: Cuaderno mobile (commit 9a0c41b). Primera reutilización: Direcciones mobile (commit 031c209).
Principio general
En mobile, mostrar primero una lista simplificada solo con nombres. Al tocar un ítem, navegar a una vista detalle full-screen. El botón back del navegador vuelve a la lista.
Estructura de vistas
window.innerWidth > 768
└─ Vista DESKTOP: split layout (1fr 420px)
window.innerWidth ≤ 768 && selectedId === null
└─ Vista MOBILE LISTA: <ul> compacto (nombre + chip + chevron)
window.innerWidth ≤ 768 && selectedId !== null
└─ Vista MOBILE DETALLE: position: fixed; inset: 0; z-index: 40
URL como fuente de verdad
La URL refleja siempre el estado visible, lo que permite:
- Compartir un enlace directo a una ficha (
?id=N). - Que el historial del navegador funcione correctamente (back = cierra detalle).
- Que una recarga de página restaure el estado correcto.
/tools/<nombre> → vista lista
/tools/<nombre>?id=N → vista detalle (ficha N)
Primitivas de URL
// Lee el id de la URL actual
function readIdFromUrl(): number | null {
const sp = new URLSearchParams(window.location.search);
const raw = sp.get('id');
if (!raw) return null;
const n = parseInt(raw, 10);
return Number.isFinite(n) ? n : null;
}
// Escribe el id en la URL (pushState, NO reload)
function pushIdToUrl(id: number | null) {
const sp = new URLSearchParams(window.location.search);
if (id === null) sp.delete('id'); else sp.set('id', String(id));
const qs = sp.toString();
const newUrl = `${window.location.pathname}${qs ? '?' + qs : ''}`;
if (newUrl !== window.location.pathname + window.location.search) {
window.history.pushState({ id }, '', newUrl);
}
}
Hooks necesarios
// 1. Detección de breakpoint reactiva
const [isMobile, setIsMobile] = useState<boolean>(() => detectMobile());
useEffect(() => {
const onResize = () => setIsMobile(detectMobile());
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
// 2. Sincronización URL ↔ estado al montar y en popstate
useEffect(() => {
if (!isMobile) return;
const initial = readIdFromUrl();
if (initial !== null) setSelectedId(initial);
const onPop = () => setSelectedId(readIdFromUrl());
window.addEventListener('popstate', onPop);
return () => window.removeEventListener('popstate', onPop);
}, [isMobile]);
Comportamiento fetchAll en mobile
Las tools con auto-selección del primer ítem en desktop deben no auto-seleccionar en mobile (para que arranquen en vista lista):
setSelectedId((current) => {
if (current && data.find((d) => d.id === current)) return current;
if (detectMobile()) return null; // mobile arranca en lista
return data[0]?.id ?? null; // desktop auto-selecciona
});
CSS base del patrón
/* Lista compacta */
.directions-mobile-name-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.directions-mobile-name-row { display: flex; align-items: center; gap: 12px; min-height: 56px; /* tap area */ }
/* Detalle full-screen */
.directions-mobile-detail {
position: fixed; inset: 0; z-index: 40;
background: var(--bg);
display: flex; flex-direction: column;
}
.directions-mobile-bar {
backdrop-filter: blur(20px);
padding: calc(8px + env(safe-area-inset-top)) 12px 8px; /* safe area iOS */
border-bottom: 1px solid var(--border-soft);
}
.directions-mobile-body { flex: 1; overflow-y: auto; padding: 1rem; }
Corrección crítica breakpoint tablet
Al añadir mobile, el colapso de columnas desktop debe acotarse para no activarse en móvil:
/* ✗ Antes */
@media (max-width: 1100px) { .split { grid-template-columns: 1fr; } }
/* ✓ Después — solo tablet */
@media (max-width: 1100px) and (min-width: 769px) { .split { grid-template-columns: 1fr; } }
Checklist de implementación en una nueva tool
- Añadir
detectMobile()+MOBILE_BREAKPOINT = 768al fichero del componente. - Añadir
isMobilestate + listenerresize. - Añadir
readIdFromUrl()+pushIdToUrl()+ listenerpopstate. - Wrap de apertura:
openItem(id)que llamasetSelectedId+pushIdToUrl. -
closeDetail()que llamasetSelectedId(null)+pushIdToUrl(null). - Guardar instrucción
if (detectMobile()) return nullenfetchAll. - Extraer componente
<ItemDetail>reutilizable (desktop aside + mobile body). - Añadir bloque CSS
@media (max-width: 768px)con clases*-mobile-*. - Corregir breakpoint tablet
and (min-width: 769px).
Tools que implementan este patrón
| Tool | Commit | Fecha |
|---|---|---|
| Cuaderno | 9a0c41b | anterior a 2026-05-13 |
| Direcciones | 031c209 | 2026-05-13 |
Véase también
- [[feature—directions—mobile-lista-detalle]]