SilsX sin Bootstrap: cómo migramos el frontend al kit propio
Actualizado 2026-10-10: la migración quedó terminada en SilsX. Se cerraron la Fase 3 y la
Fase 4 (Bootstrap y bootstrap-vue desinstalados, sin prefijo tw: y sin important), y se
sumaron aprendizajes nuevos.
El frontend de SilsX (dashboard-k2) nació sobre Bootstrap 4 + bootstrap-vue 2. Desde hace
un tiempo convive con un kit de componentes propio en src/components/ui/ (Tailwind 4,
puerto Vue 2.7 del design system de SilsX, galería en /#/ui). En septiembre/octubre de 2026
armamos un plan por fases para sacar Bootstrap del todo, y hoy está ejecutado completo: no
quedan componentes de bootstrap-vue, ni el paquete bootstrap, ni el prefijo de Tailwind.
Este post cuenta la estrategia, qué se hizo en cada fase, cómo verificamos que nada cambiara visualmente, y los problemas que aparecieron en el camino para que nadie se los vuelva a comer.
Estado general
| Fase | Qué | Estado |
|---|---|---|
| 0 | Groundwork: toasts, allowlist de @source, auditoría de colisiones, sacar @silstech/sils-design | ✅ Hecho |
| 1 | Primitivos que faltaban en el kit: Modal, Collapse, Progress, v-tooltip, Carousel | ✅ Hecho |
| 2 | Wrappers de alto apalancamiento: Table.vue, shared/Table.vue, ABMComponent.vue | ✅ Hecho |
| 3 | Migración módulo por módulo | ✅ Hecho (todos los módulos) |
| 4 | Limpieza final: desinstalar Bootstrap, sacar el prefijo tw: y el important | ✅ Hecho |
Lo que queda son seguimientos chicos, no bloqueantes:
- QA manual con el plan de
QA-migracion-bootstrap.md(casos por sección, cambios intencionales y bugs conocidos que no hay que reportar). - Scopear algunas reglas CSS globales que se filtran entre vistas (
.scroll-container,.search-videoplayback-box,.trapecio-0,.label-detail,.btn-buscar,.style-titleTab-2). Hoy se compensan con el modificador!en los lugares afectados. - Bugs que ya existían antes de la migración, encontrados por los specs y anotados en el plan para corregirlos aparte.
- Limpiezas menores: overrides sin markup en
_custom.scss, algunos archivos huérfanos y comentarios viejos que describen la cascada anterior.
Las fuentes de verdad, todas en el repo dashboard-k2:
- El plan:
qu-cambios-habr-a-que-vivid-tome.md(fases, estado fechado de cada punto, bugs encontrados, verificaciones con sus números). - La guía del kit:
src/components/ui/README.md, secciones "Migrar desde Bootstrap: tabla de equivalencias" (decisiones D1–D11, tablas y checklist por archivo) y "!important: quién le gana a quién" (las capas de CSS actuales). - El QA manual:
QA-migracion-bootstrap.md.
La estrategia: convivir, no big-bang
Al arrancar la Fase 3 quedaban 328 archivos con <b-*> y no había tests visuales, así que
reescribir todo de una era la forma más segura de romper layouts en silencio. En cambio:
- Prefijo
tw:+importantmientras durara la convivencia. Tailwind se importaba como@import "./tw-index.css" source(none) prefix(tw) important;: las utilidades del kit se escribíantw:flex,tw:mt-2… y no chocaban con las clases de Bootstrap; al ser!importantle ganaban a cualquier regla de Bootstrap, del_custom.scsso de un<style>. Era andamiaje: en la Fase 4 se sacaron las dos cosas (ver abajo). - Allowlist con
source(none). Tailwind sólo escanea lo listado con@sourceensrc/assets/css/ui.css. Un archivo fuera de la lista no recibe ninguna utilidad, sin error ni warning. Hoyviews,components,containersylayoutsestán cubiertos como carpetas enteras. - Sin preflight global. El reset vive scopeado a
.silsx-root; el resto de la app sigue con el reboot de Bootstrap (que es lo único que quedó de Bootstrap, ver Fase 4). - Tests de caracterización antes de tocar. Antes de migrar cada módulo se escriben specs
e2e con Playwright (
tests/e2e/) que fijan el comportamiento actual, y se corren antes y después. Los specs usan un mutation guard (tests/e2e/utils/mutation-guard.js) que bloquea toda escritura contra staging, y un login por worker para no agotar la cuota de Firebase. - Diff de estilos computados para los cambios globales. Cuando el cambio no es de un
módulo sino del CSS de toda la app (sacar un paquete, el prefijo, el
important), los e2e no alcanzan. Ahí comparamos los estilos computados de cada elemento visible entre el build de antes y el de después (detalle en "Lo que aprendimos"). - Un módulo a la vez, siguiendo una única tabla de equivalencias. Si un caso no está en la tabla, se agrega ahí antes de inventarlo en una vista.
Fase 0 — Groundwork ✅
Commits Fase 0: cambios en tooltips + notificaciones y Fase 0: segunda parte.
- Toasts unificados.
$bvToasty<b-toast>quedaron en cero. La API oficial eseventBus.$emit('show-toast', { type, message }, extra), queDefaultContainer.vuetraduce a vue-toastification. De paso se arreglaron dos bugs: el handler deshow-toast-onceusaba una variable que no recibía (esos toasts nunca se mostraban) y el deduplicado demain.jsmezclaba toasts sincustomId. - El allowlist de
@sourcepasó a ser real. Hallazgo: el import no teníasource(none), así que Tailwind autodetectaba todo el repo y la lista era decorativa. Se verificó que el CSS emitido fuera el mismo antes y después. - Auditoría de colisiones. Se borraron un bloque de Tailwind inerte en
style.scssy un parche muerto de.collapse. La fuente real de colisiones era otra: - Se sacó
@silstech/sils-design. Subundle.cssera un volcado viejo de estilos (con una copia de font-awesome adentro) que pisaba a Bootstrap sin prefijo. Las pocas reglas que realmente ganaban se pasaron a_custom.scsscon los tokens del kit y el paquete se desinstaló. Hoy no hay componentes globales: un<Button>sin suimportdel kit queda como<button>nativo.
Fase 1 — Primitivos ✅
Commit Fase 1: modales y sils-design. Se construyeron en el kit, con el patrón ya
establecido (<script setup>, uiClass, cva, sin registro global), las piezas que faltaban:
Modal(ui/modal/):v-model, portal propio, foco atrapado, lock de scroll yModalEventcancelable como el de bootstrap-vue. Por defecto no cierra con click en el fondo y viene centrado.Collapse/CollapseTrigger/CollapseGroup(ui/collapse/): estado local, sin bus de eventos ni ids globales.Progress/ProgressBar(ui/progress/).- Directiva
v-tooltip: reemplazo drop-in dev-b-tooltip, con los mismos modificadores. Es la única pieza registrada global. Carousel/CarouselSlide(ui/slider/), que se terminó de construir en la Fase 3 para Kilómetros.
Cada pieza nueva entra a la galería /#/ui con todos sus estados, y tiene specs unitarios
(Vitest) y e2e (design-system-*.spec.js).
Fase 2 — Wrappers de alto apalancamiento ✅
Commit Fase 2. Migrar primero los componentes que usan decenas de vistas:
KitDataTable(src/components/shared/KitDataTable.vue): mismo contrato que<b-table>(slotscell(key)/head(key),row-clicked,row-selected, sort con.sync…) sobre los primitivosTable*del kit. El comparador de bootstrap-vue se replicó ensrc/shared/sorting-tools.js.ABMComponent.vue: sus 14<b-modal>pasaron alModaldel kit, con el estado en un objetomodals.*en vez dev-b-modal/$bvModal/bv::hide::modal.
Antes de tocarlos se escribieron abm-table-modals.spec.js y
shared-table-infractions.spec.js. Esos specs encontraron bugs que ya existían (ver abajo).
Fase 3 — Módulo por módulo ✅
Un módulo por vez, con su spec de caracterización antes:
views/ABMS(commitsFase 3: abmsyFase 3 migración de abms finalizada), en cuatro grupos (vehículos/catálogos, viajes/tramos/hoja de ruta, alertas/reglas, usuarios/roles).views/Reports+components/reports(commitReports), y<b-progress>en cero en toda la app.- Kilómetros (commit
kilometers): el último consumidor de@silstech/sils-design. components/sharedy el resto decomponents/: filtros desplegables sobrePopover,GoogleMaps.vue,TableWithTimeline.vue, selectores de fecha, rankings, etc.- Vistas finales (commits
Sigo con fase 3…Fin fase 3), en seis grupos en paralelo: turnos y calendario de mantenimiento; mantenimiento online; índices (ICM, ISP, PC, IJL, ISF y sus detalles); incidentes y avisos; TechnicalMonitoring, Dashboard, Configuraciones y tableros; infracciones, video y el shell (DefaultContainer,DefaultViewLayout).
En el pase de cierre, las ~25 entradas sueltas de @source para vistas se reemplazaron por
carpetas recursivas. Ese cambio destapó 6 utilidades que vistas ya migradas usaban y nunca se
habían emitido (por ejemplo un gap-[14px] de RecorteVideos): justo el fallo silencioso
del que avisa el allowlist.
Fase 4 — Limpieza final ✅
Con la Fase 3 cerrada, lo que quedaba eran restos puntuales y el andamiaje de la convivencia.
Commits Saco typehead, Fase 3 y 4, casi final, fase 4 parte 1, Eliminación completa de bootstrap, primer paso del último paso, fase 4 cont, Reemplazo de los prefijos tw,
Fase 4 , cont y Fin paso 3. En orden:
| Paso | Qué se hizo |
|---|---|
| Typeahead fuera | SearchAutocomplete (components/shared/) reemplaza a vue-bootstrap-typeahead y vue-typeahead-bootstrap en 15 consumidores; el Autocomplete del kit cubre las listas en memoria. Librerías desinstaladas |
| Últimos restos de bootstrap-vue | Adiciones al kit: Checkbox con indeterminate, TagsInput (reemplaza <b-form-tags>), Popover trigger="hover" y Modal que queda sin transform una vez abierto (así un hijo position: fixed vuelve a ir contra la ventana). Migrados el navbar del header, el último <b-modal>, el switch de ABMGroups y el cartel de capas de GoogleMaps |
| Archivos muertos | 42 archivos + 1 spec borrados, re-grepeando cada uno (imports, rutas, <component :is>, tests) |
bootstrap-vue desinstalado | Fuera de main.js, de los specs unitarios y de package.json |
bootstrap desinstalado | El CSS que la app todavía usaba quedó como subset compilado en src/assets/scss/_bootstrap-legacy.scss, en la misma posición de la cascada |
| Achicar el subset | De 579 a 138 selectores, en grupos A–E (grilla, header, componentes, utilidades, colores de tema). La grilla y las cards de shared/Card.vue pasaron a _kit-grid.scss y _kit-card.scss; los colores de $theme-colors sin token exacto, a una paleta app-*. Hoy el parcial sólo tiene el reboot, :root y el print de elementos: ninguna clase |
Sacar el prefijo tw: | Primero se renombraron las clases propias que chocaban con utilidades; después un codemod sacó 24.172 tw: en 458 archivos |
Sacar important | El CSS de la app pasó a capas (base / components / utilities) y se resolvió cada diferencia hasta cero |
El CSS total del build de producción pasó de 1.218.400 B (antes de sacar bootstrap-vue) a 880.650 B (después de sacar el prefijo).
Cada paso se verificó con npm run test:unit, build de producción y la suite e2e (smoke y
completa); desde la desinstalación de bootstrap-vue, además, con un diff de estilos computados
contra el build anterior. El resultado fue 0 diferencias reales en todos; lo único que aparece es ruido conocido (spinners en
distinto punto de la animación, teselas de Google Maps, orden de llegada de los toasts).
Suite smoke de e2e
Para poder correr los e2e después de cada paso sin esperar la suite entera, se marcó un
subconjunto con el tag @smoke (commit Agrego suite smoke): 19 tests, 1–2 por área crítica,
rápidos, sin depender de datos en vivo ni de test.skip condicionales. Corre en unos 4 minutos
(3.6–3.7 min en las corridas de la Fase 4).
- Local:
npm run test:e2e:smoke(=playwright test --grep @smoke). - Jenkins:
Jenkinsfile.e2etiene el parámetroSUITE(smokepor defecto,fullpara la suite completa), además deE2E_ENV(testing,prod,preview).
Lo que aprendimos (y conviene saber antes de migrar)
Breakpoints: los de Bootstrap, no los de Tailwind. Tailwind usa 640/1024/1280 y Bootstrap
576/768/992/1200. En código migrado se usan variantes arbitrarias (min-[576px]:,
min-[768px]:, min-[992px]:, min-[1200px]:, y max-[992px]: para "mobile"). Nunca
sm:/md:/lg:. max-[992px]: coincide exacto con el isMobile del store.
Gutters exactos. Un <b-row> es flex flex-wrap -mx-[15px] y cada columna
relative w-full px-[15px] + su ancho en %. Reemplazarlo por gap-* "porque se ve igual"
hace que la última columna salte de línea en otros anchos. Ojo también con el espaciado: mt-3
de Bootstrap (1rem) no es el mt-3 de Tailwind, es mt-4.
Caracterización e2e antes de cada módulo. Los specs escritos antes de migrar detectaron
regresiones propias (por ejemplo, las tarjetas de Reportes que dejaron de mostrar "Ver
Reporte" por cómo Vue 2 pasa atributos al Button del kit, o el ranking de ICM/ISP que dejó de
encogerse dentro de su card porque .card-body traía min-height: 1px) y también bugs que
ya existían: la búsqueda de Transportistas distinguía mayúsculas, el Histórico quedaba con el
spinner colgado ante un 403, la "X" de borrar en los modales de ICM/ISP/Aprobadas nunca
aparecía, la fecha de ISP ordenaba como texto, y había ids de collapse repetidos entre
componentes. Los que no eran de la migración quedaron anotados en el plan para corregirlos
aparte.
Los specs también se engañan solos. Un toHaveCount(0) sobre .spinner-border o
.text-warning pasa siempre cuando esa clase deja de existir. Conviene buscar por rol o texto
visible (getByRole('status'), getByText('*')), que es lo que ve el usuario.
Para el CSS global, la red es el diff de estilos computados. Sacar Bootstrap, el prefijo o
el important no rompe ningún test: cambia un margen o un color en alguna vista. Lo que
funcionó fue servir los dos builds (antes y después) en paralelo con vite preview, recorrer
/login + 43 rutas a 1440px y 10 a 390px con el usuario de e2e, y comparar unas 30
propiedades computadas de cada elemento visible (62 al achicar el subset de Bootstrap, incluyendo
::before/::after). En los pasos más delicados también con desplegables y modales abiertos y
con hover/foco sobre ~200 controles. Cada diferencia que no fuera ruido conocido había que
explicarla.
El diff destapa fugas que dependen de la sesión. Dos ejemplos: ISPDetail.vue y
PCDetail.vue tenían en un <style> sin scope .bg-alarmas { background-color: transparent !important }, que después de visitar esas rutas apagaba todos los bg-alarmas de la app;
y el .block global de WDBlock le cambiaba el ancho a unos <label> de ABMCustomAlerts
después de pasar por Conducción Nocturna. Con sesión limpia no se ven; por eso conviene correr
el diff en el mismo orden antes y después, y sospechar de todo <style> sin scoped.
Sin prefijo, ninguna clase propia puede llamarse como una utilidad. Con tw: daba igual
tener una clase propia relative, block, grow, cursor-pointer o text-ellipsis. Sin
prefijo, Tailwind ve esos nombres en los archivos escaneados y emite también la utilidad,
que se suma (o pelea) con el CSS propio. Antes de sacar el prefijo se midieron los choques con
el mismo compilador de Tailwind que usa el build y se renombraron (cursor-pointer →
clickable-cursor, relative → pos-relative, block-N → wd-block-N…). Caso especial:
KitDataTable acepta en thClass/tdClass clases de Bootstrap (text-center, pl-2,
w-50…) como API; las sigue traduciendo a utilidades, pero ya no las emite al DOM, porque
sin prefijo pl-2 sería una utilidad real con otro valor.
Sacar important: el CSS sin capa le gana a las capas. En CSS con cascade layers, una regla
sin capa le gana a cualquier regla en capa, sin importar la especificidad. Con las
utilidades !important eso no se notaba; sacando el important sin más aparecían 20.718
diferencias. El orden quedó theme < base (reboot, reset de .silsx-root, Font Awesome) <
components (_kit-grid, _kit-card, _layout) < utilities < sin capa < inline: con eso
bajaron a 1.512, y el resto se resolvió caso por caso (declaraciones muertas en <style> que
la utilidad ya pisaba, reglas de componente movidas a @layer components, y el modificador
! sólo contra CSS que no se puede tocar). Regla práctica: no duplicar en un <style> lo que
ya fija una utilidad del mismo nodo, porque ahora gana el <style>.
v-show + utilidad de display en el mismo nodo. Con las utilidades !important,
v-show ponía display: none inline, pero tw:flex ganaba y el bloque nunca se ocultaba, sin
ningún error. Desde que se sacó important el inline vuelve a ganar, pero la regla del README
se mantiene: un flex! o un display !important de otro CSS lo vuelve a romper. El v-show
va en un wrapper, o se alterna flex/hidden.
Las directivas de permisos tenían el mismo problema. v-allowed-roles,
v-required-views y compañía ocultaban con el.style.display = "none", que perdía contra un
Button del kit: botones como "Eliminar infracciones" quedaban visibles sin permiso. Se
arregló en src/directives.js con setProperty('display', 'none', 'important'), que sigue
ganando con la cascada nueva.
.silsx-root button vs. los botones de Bootstrap. Mientras convivían, el reset del
wrapper (padding 0, radio 0) le ganaba a .btn por especificidad: envolver un contenedor que
todavía tenía <b-button> dejaba esos botones sin padding ni bordes redondeados. Lo mismo con
h1–h6, p y label crudos, que pierden margen adentro del wrapper (eso sigue vigente).
e2e contra la rama, no contra staging. Los specs apuntan a staging por defecto
(.env.e2e.local usa https://test.silsx.com). Para validar un cambio que todavía no está
desplegado hay que pisarlo con E2E_BASE_URL=http://localhost:8080: varios tests del smoke
(Aprobación de Infracciones, video wall, modal de la galería, toasts) fallaban o se salteaban
contra staging y pasaban contra la rama migrada. Un smoke rojo contra staging puede significar
sólo que el cambio todavía no está desplegado ahí.
Cómo migrar un archivo (resumen)
La guía completa está en src/components/ui/README.md ("Migrar una vista" y "Migrar desde
Bootstrap"). Con las convenciones de hoy:
- Antes de tocar, asegurarse de que haya un spec e2e que cubra el comportamiento de la vista. Si no hay, escribirlo y correrlo contra el código actual.
- Las utilidades de Tailwind van sin prefijo (
flex,mt-4,bg-accent). Untw:copiado de una rama vieja no emite nada. - Verificar que el archivo esté bajo un
@sourcedesrc/assets/css/ui.css:views,components,containersylayoutsya están cubiertos; cualquier otro archivo que use utilidades (por ejemplo un.jsdeshared/) se agrega ahí. - No nombrar clases propias como utilidades (
block,relative,grow,text-center,transition…): Tailwind emitiría también la utilidad. - Respetar las capas: una regla de
<style>sin capa le gana a una utilidad del mismo nodo. Si una clase propia tiene que perder contra las utilidades que le pasa el consumidor, va en@layer components { … }; si repite lo que ya pone una utilidad, se borra. El!(flex!) sólo como último recurso, con un comentario de contra qué compite. - Envolver en
class="silsx-root bg-transparent"e importar explícitamente cada componente del kit que aparezca en el template. - Convertir con la tabla de equivalencias: grilla con los gutters y breakpoints de Bootstrap,
colores siempre a tokens (nunca hex),
classpara layout yui-classpara la pinta. Una clase de Bootstrap escrita hoy no pinta nada. - No romper hooks: conservar
id,ref,data-*y clases que usen otros archivos o los specs. - Correr los greps del "Checklist por archivo" del README,
npm run build,npm run test:unity los e2e del módulo (como mínimonpm run test:e2e:smoke), contra la rama local. - Revisión visual en 1440px y 800px (los dos lados del corte de mobile). Si el cambio toca CSS global, diff de estilos computados antes/después.