fix(studio): uMind era inusable en móvil

El SPA no tenía ningún tratamiento responsive: un sidebar fijo de 256px
siempre visible. En un teléfono de 375px eso deja 119px para el contenido.

Ahora en móvil el sidebar sale de flujo y entra deslizándose desde la
izquierda, con botón en el header y fondo oscuro para cerrarlo. Se cierra
solo al elegir un tenant: dejarlo abierto tapa justo la pantalla a la que
se acaba de entrar. En escritorio (md+) no cambia nada — sigue fijo y sin
botón.

El estado vive en lib/ui.js porque lo tocan dos componentes que no son
padre/hijo: el botón del header y el propio sidebar.

También se achica el padding del contenido en pantallas chicas y las tabs
del agente pasan a scrollear en su propia caja en vez de desbordar.

Medido a 492px de ancho: scrollWidth igual a innerWidth y ningún elemento
sobresaliendo. Escritorio verificado sin regresión.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-17 18:16:34 -05:00
co-authored by Claude Sonnet 5
parent e2440b04f1
commit ce5956878d
8 changed files with 69 additions and 33 deletions
+14 -2
View File
@@ -1,13 +1,25 @@
<script setup>
import Sidebar from './components/Sidebar.vue'
import { tema, alternarTema } from './lib/tema.js'
import { menuAbierto } from './lib/ui.js'
</script>
<template>
<div class="min-h-screen flex">
<Sidebar />
<main class="flex-1 min-w-0 flex flex-col">
<header class="h-14 shrink-0 flex items-center justify-end gap-1 px-6 border-b border-borde">
<header class="h-14 shrink-0 flex items-center gap-1 px-4 sm:px-6 border-b border-borde">
<!-- Solo en móvil: en escritorio el sidebar está siempre visible. -->
<button
class="btn-ghost !px-2 !py-1.5 md:hidden"
aria-label="Abrir menú"
@click="menuAbierto = true"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<span class="flex-1"></span>
<button
class="btn-ghost !px-2.5 !py-1.5"
:title="tema === 'oscuro' ? 'Cambiar a claro' : 'Cambiar a oscuro'"
@@ -16,7 +28,7 @@ import { tema, alternarTema } from './lib/tema.js'
<span class="text-base leading-none">{{ tema === 'oscuro' ? '☀️' : '🌙' }}</span>
</button>
</header>
<div class="flex-1 max-w-5xl w-full mx-auto px-8 py-8 animate-aparecer">
<div class="flex-1 max-w-5xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 animate-aparecer">
<router-view />
</div>
</main>
+20 -2
View File
@@ -1,8 +1,9 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { api } from '../lib/api.js'
import { apiUmind, contexto } from '../lib/contexto.js'
import { menuAbierto } from '../lib/ui.js'
const route = useRoute()
const router = useRouter()
@@ -20,6 +21,10 @@ const form = ref(vacio())
// anidada de sus agentes (/tenants/:tenantId/agentes/:agenteId).
const tenantActivoId = computed(() => route.params.tenantId || route.params.id)
// Al elegir un tenant en móvil el menú se cierra solo: dejarlo abierto tapa
// justo la pantalla a la que se acaba de entrar.
watch(() => route.fullPath, () => { menuAbierto.value = false })
function vacio() {
return { nombre: '', dominios_permitidos: '', activo: true, cliente_id: null, plan_id: null }
}
@@ -121,7 +126,20 @@ onMounted(() => {
</script>
<template>
<aside class="w-64 shrink-0 h-screen sticky top-0 flex flex-col border-r border-borde bg-superficie">
<!-- Fondo oscuro en móvil: sin esto el menú abierto se superpone al
contenido sin dejar claro que hay que cerrarlo. -->
<div
v-if="menuAbierto"
class="fixed inset-0 bg-black/50 z-30 md:hidden"
@click="menuAbierto = false"
></div>
<aside
class="w-64 shrink-0 flex flex-col border-r border-borde bg-superficie
fixed inset-y-0 left-0 z-40 transition-transform duration-200
md:static md:h-screen md:sticky md:top-0 md:translate-x-0"
:class="menuAbierto ? 'translate-x-0' : '-translate-x-full'"
>
<div class="h-14 px-4 flex items-center border-b border-borde">
<router-link to="/" class="text-base font-semibold text-texto">
uMind <span class="text-brand">Studio</span>
+6
View File
@@ -0,0 +1,6 @@
import { ref } from 'vue'
// Estado del menú lateral en móvil. Vive acá y no en App.vue porque lo tocan
// dos componentes que no son padre/hijo: el botón del header y el propio
// sidebar, que se cierra solo al navegar.
export const menuAbierto = ref(false)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>uMind Studio</title>
<script type="module" crossorigin src="/orchestrator/assets/index-DsDQ67K1.js"></script>
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-DyYio0bD.css">
<script type="module" crossorigin src="/orchestrator/assets/index-B67EqHas.js"></script>
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-D_GQdYSJ.css">
</head>
<!-- Sin clase de fondo: el color lo pone body en style.css desde los tokens,
que son los que cambian con el tema. Una utilidad acá le ganaba a la