Hola ![]()
La idea básica
El objetivo era construir un cargador de esqueleto (skeleton loader) que se genere a partir de la interfaz de usuario real de Discourse, en lugar de depender de una plantilla de esqueleto codificada.
El creador permite a un administrador seleccionar elementos reales en la página y convertirlos en regiones de esqueleto.
Por ejemplo:
.title
.avatar
.topic-excerpt
.btn
.category-breadcrumb
El componente luego utiliza esos selectores para generar el esqueleto en tiempo de ejecución.
Vista previa del esqueleto
La parte interesante es que el administrador no tiene que escribir los selectores manualmente. El creador analiza el elemento seleccionado y genera varios selectores candidatos.
Generar selectores útiles resultó ser más difícil de lo esperado
Uno de los primeros problemas con los que me topé fue la generación de selectores.
Una implementación ingenua puede producir fácilmente algo como:
.container.list-container.--topic-list .row.full-width .contents ...
Técnicamente válido, pero demasiado específico para una configuración de esqueleto reutilizable.
Puede empeorar aún más con los iconos, donde el selector generado puede incluir detalles de implementación como clases relacionadas con SVG.
Lo que realmente quería era algo más cercano a:
.badge-category__name
o:
.badge-category__wrapper .d-icon
en lugar de un selector que describa toda la ruta del DOM.
Por lo tanto, el creador ahora genera varios candidatos y les asigna una puntuación basándose en cosas como:
- profundidad del selector
- número de clases
- coincidencias repetidas
- clases relacionadas con el estado
- clases técnicas de SVG/iconos
- si el selector aún coincide con el elemento seleccionado
El resultado es una lista de selectores recomendados de los que el administrador puede elegir o editar manualmente.
Elementos ocultos
También hay un selector separado para elementos que simplemente deben desaparecer mientras se muestra el esqueleto.
Por ejemplo:
.alert.alert-info
Esto resultó ser útil para cosas como banners de anuncios o avisos temporales que existen durante la construcción/pruebas pero no deben afectar el diseño del esqueleto.
Un problema interesante aquí fue que ocultar un elemento no debe dejar un espacio vacío detrás.
Por lo tanto, los elementos excluidos no se tratan simplemente como una lista de display: none; el cálculo de la geometría también debe entender que el elemento no forma parte del diseño final.
Vista previa del esqueleto
Navegación
Probablemente el mayor desafío fue la navegación.
El comportamiento deseado era:
click
↓
mostrar esqueleto inmediatamente
↓
Discourse cambia la ruta
↓
aparece el DOM de destino
↓
ocultar esqueleto
La solución tentadora era engancharse profundamente en el ciclo de vida de la navegación y esperar a que el DOM se estabilizara por completo.
Eso resultó ser el enfoque equivocado.
En un momento dado, el esqueleto podía permanecer visible durante varios segundos después de que el contenido real ya estuviera allí.
La lección fue simple:
El esqueleto no debe convertirse en una puerta de preparación del DOM.
Una vez que el destino tiene suficiente contenido real para tomar el control, el esqueleto debe apartarse.
Eso hizo una gran diferencia en la velocidad percibida de la navegación.
Viewports
Discourse ya tiene un sistema de viewport responsivo, por lo que el componente ahora utiliza la misma abstracción de puntos de ruptura (breakpoints):
xs
sm
md
lg
xl
2xl
La configuración del esqueleto también puede agruparse como:
móvil → xs / sm
tablet → md
desktop → lg / xl / 2xl
todo → todo
Esto significa que el componente no necesita conocer los valores de píxeles reales en absoluto.
Si Discourse cambia los valores de los puntos de ruptura, el componente de esqueleto no tiene que ser reescrito alrededor de nuevos números codificados.
Caché de geometría
Los selectores nos dicen qué debe renderizarse, pero no nos dicen exactamente dónde deben aparecer las formas del esqueleto.
Para eso, agregué la captura de geometría.
El creador puede medir las regiones renderizadas reales y almacenar su geometría para que el cargador pueda renderizar un esqueleto de destino inmediatamente durante la navegación SPA.
También hay una opción de bloqueo de geometría explícita para casos en los que no quiero que las visitas posteriores cambien continuamente la geometría de referencia.
Esta fue otra distinción importante:
definición de selectores y geometría renderizada son dos cosas diferentes.
Borradores
Otra cosa que se volvió necesaria fue el estado de borrador.
No quería este flujo de trabajo:
abrir creador
→ gastar 10 minutos configurándolo
→ cerrar creador
→ todo desaparece
Por lo tanto, el creador mantiene un borrador en progreso separado de la configuración real del tema.
El borrador está limitado a la combinación de página/ruta/viewport, por lo que, por ejemplo:
topic-list / lg
topic-list / md
topic-list / xs
no se sobrescriben accidentalmente entre sí.
Cerrar el creador no destruye el trabajo realizado.
Deshacer
Una vez que el creador se volvió más interactivo, un sistema de deshacer se volvió casi inevitable.
El creador almacena instantáneos de su estado de configuración:
{
"regions": \[\],
"excludes": \[\]
}
en lugar de intentar mantener un historial de operaciones del DOM.
Eso hace que el sistema de deshacer sea mucho más fácil de razonar y también lo mantiene independiente del DOM real de la página.
Este proyecto está en desarrollo activo. ¡Espero que pronto esté listo para un componente de tema! ![]()




