diff --git a/CHANGELOG_SESSION.md b/CHANGELOG_SESSION.md
index d393a30..4c76f46 100644
--- a/CHANGELOG_SESSION.md
+++ b/CHANGELOG_SESSION.md
@@ -471,6 +471,23 @@
* **Batería de Pruebas Automatizadas:**
* Creación de tests unitarios y de integración en `tests/test_services_and_dto.py`, `tests/test_jwt_and_api.py` y `tests/test_security_chain.py` (18/18 pruebas aprobadas).
+---
+
+## 28. Selector de Vista Universal: Tabla / Grilla vs. Tarjetas / Mosaico (2026-09-05)
+* **Arquitectura Extensible de Visualización (`app/static/js/view_switcher.js` y `app/templates/partials/view_switcher.html`):**
+ * Desarrollo del módulo `ViewSwitcherManager` que desacopla la lógica de presentación de los registros. Permite añadir futuras vistas (ej. kanban, timeline, compacta) mediante atributos de datos `data-view` y `data-view-container` sin tocar el backend.
+ * **Conmutación Inmediata:** Cambio instantáneo de vista en el cliente sin recarga de página.
+ * **Persistencia de Preferencias:** Almacenamiento local mediante `localStorage` (`view_preference_{context}`) para conservar la elección del usuario entre sesiones.
+ * **Preservación de Estado y Filtros:** Inyección reactiva del parámetro `display` en formularios de búsqueda/filtro y actualización silenciosa de la URL (`window.history.replaceState`), garantizando que búsquedas, ordenamientos y paginaciones conserven la vista activa.
+* **Integración en Módulo de Aulas y Espacios (`app/templates/classrooms/list.html`):**
+ * **Vista de Tabla / Grilla:** Columnas detalladas (Código, Edificio/Sede, Piso, Capacidad, Equipamiento, Ocupación D/S/M, Estado y Acciones).
+ * **Vista de Tarjetas / Mosaico:** Cuadrícula responsive (1 a 4 columnas) con badges semánticos, indicadores de piso y barras de porcentaje de uso.
+* **Integración en Módulo de Reservas y Cronograma (`app/templates/schedule/list.html`):**
+ * **Vista de Tabla:** Análisis denso de reservas con estado, comisión, horario y aula.
+ * **Vista de Tarjetas:** Fichas visuales con badges destacados de fecha/hora, materia, turno, docente a cargo y enlaces a aulas virtuales.
+* **Pruebas y Verificación:**
+ * Incorporación de tests en `tests/test_view_switcher.py`, validando renderizado de selectores y contenedores en ambas rutas (20/20 pruebas aprobadas).
+
diff --git a/README.md b/README.md
index 0fcec62..2335009 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,21 @@ El menú desplegable **Gestión** en la barra de navegación organiza las áreas
* **Tipificaciones de Hitos:** Catálogo de tipos de hitos de evaluación académica.
* **Sincronización Sheets:** Integración con Google Sheets para actualización en lote.
+### 🔀 9. Selector de Vista Universal (Tabla / Grilla vs. Tarjetas / Mosaico)
+La plataforma implementa una arquitectura desacoplada de visualización (`app/static/js/view_switcher.js` y `app/templates/partials/view_switcher.html`) que permite al usuario alternar instantáneamente entre diferentes formatos de presentación sin modificar los datos ni perder los filtros o búsquedas activas:
+* **Vista de Tabla / Grilla (`▤ Tabla`):**
+ * Presentación tabular en filas y columnas de alta densidad informativa.
+ * Muestra múltiples atributos simultáneamente (edificio, piso, aforo, métricas de ocupación, estado, acciones).
+ * Ideal para consulta y comparación masiva de registros.
+* **Vista de Tarjetas / Mosaico (`▦ Tarjetas`):**
+ * Presentación visual en tarjetas independientes distribuidas automáticamente según el ancho de pantalla (1 a 4 columnas).
+ * Enfoque en la información destacada con badges semánticos, barras de uso y botones de acceso rápido.
+* **Características del Selector:**
+ * **Cambio Inmediato:** Conmutación instantánea en el cliente sin recarga de página (`d-none` / manipulación de DOM).
+ * **Persistencia de Preferencia:** Memorización automática mediante `localStorage` para conservar la elección del usuario en futuras visitas.
+ * **Preservación de Filtros:** Inyección automática del parámetro `display` en formularios y sincronización de URL sin recarga (`history.replaceState`).
+ * **Arquitectura Extensible:** Permite registrar futuras vistas (kanban, calendario, compacta) mediante atributos `data-view` y `data-view-container` sin alterar la lógica de negocio.
+
---
## 🛠️ Stack Tecnológico
diff --git a/app/static/css/style.css b/app/static/css/style.css
index bf38817..7952e30 100644
--- a/app/static/css/style.css
+++ b/app/static/css/style.css
@@ -174,4 +174,36 @@
background-color: #FFFFFF !important;
color: #000000 !important;
}
+}
+
+/* View Switcher Controls */
+.view-switcher-group {
+ flex-direction: row !important;
+ display: inline-flex !important;
+}
+
+.view-switcher-group .btn {
+ margin-bottom: 0 !important;
+ font-weight: 600;
+ font-size: 0.82rem;
+ padding: 0.35rem 0.75rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease-in-out;
+}
+
+.view-switcher-group .btn.active {
+ box-shadow: 0 2px 6px rgba(var(--brand-primary-rgb), 0.25);
+}
+
+.reservation-card {
+ transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
+ border-radius: 0.75rem;
+}
+
+.reservation-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.08) !important;
+ border-color: var(--bs-primary) !important;
}
\ No newline at end of file
diff --git a/app/static/js/view_switcher.js b/app/static/js/view_switcher.js
new file mode 100644
index 0000000..64ab135
--- /dev/null
+++ b/app/static/js/view_switcher.js
@@ -0,0 +1,139 @@
+/**
+ * ViewSwitcher — Gestor Universal y Extensible de Modos de Visualización
+ *
+ * Permite alternar instantáneamente entre vista de Tabla (grilla densa)
+ * y vista de Tarjetas (mosaico responsive), preservando filtros, búsquedas
+ * y persistiendo la preferencia del usuario en localStorage.
+ */
+
+(function () {
+ 'use strict';
+
+ class ViewSwitcherManager {
+ constructor() {
+ this.init();
+ }
+
+ init() {
+ document.addEventListener('DOMContentLoaded', () => {
+ this.setupViewSwitchers();
+ });
+ }
+
+ setupViewSwitchers() {
+ const switchers = document.querySelectorAll('.view-switcher-group');
+ if (!switchers.length) return;
+
+ switchers.forEach(group => {
+ const context = group.getAttribute('data-context') || 'default';
+ const defaultView = group.getAttribute('data-default-view') || 'cards';
+
+ // Determinar vista inicial:
+ // 1. URL search param (?display=...)
+ // 2. localStorage
+ // 3. defaultView del contenedor
+ const urlParams = new URLSearchParams(window.location.search);
+ const urlView = urlParams.get('display');
+ const storedView = localStorage.getItem(`view_preference_${context}`);
+ const activeView = urlView || storedView || defaultView;
+
+ this.applyView(context, activeView, false);
+
+ // Asignar listeners a los botones del selector
+ const buttons = group.querySelectorAll('.view-switcher-btn');
+ buttons.forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const targetView = btn.getAttribute('data-view');
+ if (targetView) {
+ this.applyView(context, targetView, true);
+ }
+ });
+ });
+ });
+ }
+
+ /**
+ * Aplica la vista seleccionada en el contexto dado.
+ * @param {string} context - Identificador del módulo (ej: 'classrooms', 'reservations')
+ * @param {string} viewName - Nombre de la vista ('table', 'cards', etc.)
+ * @param {boolean} persist - Si debe persistirse en localStorage y actualizar URL
+ */
+ applyView(context, viewName, persist = true) {
+ // 1. Actualizar botones activos en el grupo selector
+ const group = document.querySelector(`.view-switcher-group[data-context="${context}"]`);
+ if (group) {
+ const buttons = group.querySelectorAll('.view-switcher-btn');
+ buttons.forEach(btn => {
+ const btnView = btn.getAttribute('data-view');
+ if (btnView === viewName) {
+ btn.classList.remove('btn-outline-secondary');
+ btn.classList.add('btn-primary', 'active');
+ btn.setAttribute('aria-pressed', 'true');
+ } else {
+ btn.classList.remove('btn-primary', 'active');
+ btn.classList.add('btn-outline-secondary');
+ btn.setAttribute('aria-pressed', 'false');
+ }
+ });
+ }
+
+ // 2. Conmutar visibilidad de los contenedores de vista
+ const containers = document.querySelectorAll(`[data-view-context="${context}"]`);
+ let matched = false;
+
+ containers.forEach(container => {
+ const containerView = container.getAttribute('data-view-container');
+ if (containerView === viewName) {
+ container.classList.remove('d-none');
+ container.removeAttribute('aria-hidden');
+ matched = true;
+ } else {
+ container.classList.add('d-none');
+ container.setAttribute('aria-hidden', 'true');
+ }
+ });
+
+ // Fallback: si la vista solicitada no tiene contenedor, restaurar a la primera visible
+ if (!matched && containers.length > 0) {
+ containers[0].classList.remove('d-none');
+ viewName = containers[0].getAttribute('data-view-container') || 'cards';
+ }
+
+ // 3. Persistir preferencia y actualizar formularios y URL
+ if (persist) {
+ try {
+ localStorage.setItem(`view_preference_${context}`, viewName);
+ } catch (e) {
+ console.warn('localStorage no disponible para guardar preferencia de vista:', e);
+ }
+
+ // Actualizar parámetro display en la URL sin recargar
+ const url = new URL(window.location.href);
+ url.searchParams.set('display', viewName);
+ window.history.replaceState({}, '', url);
+ }
+
+ // 4. Sincronizar campo hidden 'display' en formularios de búsqueda/filtro
+ const forms = document.querySelectorAll('form');
+ forms.forEach(form => {
+ let hiddenInput = form.querySelector('input[name="display"]');
+ if (!hiddenInput) {
+ hiddenInput = document.createElement('input');
+ hiddenInput.type = 'hidden';
+ hiddenInput.name = 'display';
+ form.appendChild(hiddenInput);
+ }
+ hiddenInput.value = viewName;
+ });
+
+ // 5. Emitir evento personalizado para componentes dinámicos
+ window.dispatchEvent(new CustomEvent('viewModeChanged', {
+ detail: { context, view: viewName }
+ }));
+ }
+ }
+
+ // Instanciar singleton
+ window.ViewSwitcher = new ViewSwitcherManager();
+})();
diff --git a/app/templates/base.html b/app/templates/base.html
index e0c079d..c99e669 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -208,6 +208,8 @@
+
+