# Navegación y composición

Conecta la URL y las intenciones de navegación del shell con el montaje, desmontaje y estado de tus aplicaciones.

<span class="section-anchor" id="section-1"></span>

## El shell es dueño de la navegación

El shell decide qué aplicación corresponde a cada ruta y utiliza montaje, desmontaje, eventos y estado para componerla. Astro puede resolver páginas; un router React/Vue puede gestionar rutas internas de una aplicación. El core WU no incluye `wu.router` ni `wu.navigate()`: conecta el mecanismo de navegación que utilice tu shell con su lifecycle.

`metadata.routes` en `wu.json` es información descriptiva. El parser valida strings, duplicados y límites, pero no instala handlers de History API. `wu.routes` no vacío se rechaza en Framework 4 y no debe usarse como receta de routing.

<span class="section-anchor" id="section-2"></span>

## Dos niveles de rutas

| Nivel | Ejemplo | Responsable |
| --- | --- | --- |
| Página del portal | `/docs/framework/state` | Router de archivos Astro |
| Sección dinámica del shell | `/workspace/catalog` | Controlador/router del shell |
| Vista interna de una app | `/products/:id` dentro de catalog | Router de esa app |

La URL base de descarga, como `/apps/catalog`, es un recurso técnico y no tiene por qué coincidir con la ruta de navegación. Define por separado el base del bundle, el base del router interno y la ruta que el usuario ve.

<span class="section-anchor" id="section-3"></span>

## Conectar una intención de navegación

Este ejemplo implementa una convención del shell; `shell:navigate` no activa un router integrado:

```js
const routeTable = new Map([
  ['/workspace/catalog', 'catalog'],
  ['/workspace/reports', 'reports'],
]);

let activeApp = null;
let transition = Promise.resolve();

function showRoute(path) {
  transition = transition.catch(() => {}).then(async () => {
    const nextApp = routeTable.get(path);
    if (!nextApp || nextApp === activeApp) return;
    if (activeApp) await wu.unmount(activeApp, { force: true });
    activeApp = null;
    await wu.mount(nextApp, '#workspace-slot');
    activeApp = nextApp;
  });
  return transition;
}

const stop = wu.on('shell:navigate', ({ data }) => {
  if (!routeTable.has(data?.path)) return;
  history.pushState(null, '', data.path);
  showRoute(data.path).catch(console.error);
});

const onPopState = () => showRoute(location.pathname).catch(console.error);
window.addEventListener('popstate', onPopState);
await showRoute(location.pathname);

// Al retirar el controlador del shell:
// stop();
// window.removeEventListener('popstate', onPopState);
```

La tabla limita qué rutas y apps acepta. La cola conserva orden cuando el usuario navega varias veces antes de terminar una carga. El core también coordina ownership del contenedor; mantener un estado claro en el shell evita que una promesa vieja actualice controles de navegación de una ruta nueva.

<span class="section-anchor" id="section-4"></span>

## Elementos dentro de Shadow DOM

`wu.mount(name, selector)` resuelve el selector con `document.querySelector`. Ese selector no cruza ShadowRoots y la firma actual espera un string, no un HTMLElement. Para que el shell localice el slot, colócalo en su light DOM o utiliza una composición que exponga un nodo accesible por selector.

Dentro de un lifecycle, usa el `container` entregado para manipular la raíz local; no vuelvas a buscarla globalmente por ID. El adapter Vue del repositorio ilustra por qué una raíz estable debe ser un hermano del indicador de loading y no su contenedor gestionado por el shell.

<span class="section-anchor" id="section-5"></span>

## Keep-alive y cambio de ruta

`hide` preserva estado y listeners; funciona cuando mantienes el contenedor de la app. Cambiar muchas apps sobre un único slot requiere una decisión explícita: desmontar el dueño anterior o conservar contenedores separados para cada app que permanezca viva. No uses keep-alive como sinónimo de cachear módulos; son recursos distintos.

Si Astro reemplaza páginas mediante navegación cliente, el bootstrap debe acompañar ese lifecycle: libera listeners y mounts de la página anterior y conecta los nuevos slots. Una importación de script deduplicada no significa que todos los nodos futuros queden montados automáticamente.

<span class="section-anchor" id="section-6"></span>

## Roles de UI

`wu.setPrincipal({ id: 'u1', roles: ['catalog-reader'] })` alimenta `wu.can('catalog')`. El montaje consulta los roles requeridos por el manifest y puede rechazar con `WU_ACCESS_DENIED`. Esto ayuda a mantener navegación y vistas coherentes; las APIs de datos deben autorizar cada petición en el servidor.

<span class="section-anchor" id="section-7"></span>

## Referencia de implementación

Validación de rutas descriptivas: `src/core/wu-manifest.js`; acceso: `src/core/wu-core.js` y `src/core/wu-core.js`; selector: `src/core/wu-core.js`; transición de contenedor: `src/core/wu-core.js`; convención usada por MCP: `src/core/wu-mcp-bridge.js`. Pruebas: `tests/unit/shared-container-navigation.test.js`, `tests/unit/sandbox-container-ownership.test.js`, `tests/unit/rbac.test.js`.