From 1e6e7fecdd192210da180f1c83333747a6f47746 Mon Sep 17 00:00:00 2001 From: Milton Gonzalez Date: Thu, 3 Sep 2026 11:21:07 -0500 Subject: [PATCH] =?UTF-8?q?fix(ui):=20reemplaza=20panel-debajo-del-listado?= =?UTF-8?q?=20por=20DetailPopup=20de=20tama=C3=B1o=20fijo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El detalle de un item de listado (actividad, departamento, usuario, proyecto nuevo, catalogo) ya no se apila debajo ni al costado del listado: ahora abre en un popup que cubre la ventana, deja el listado inactivo detras, nunca se cierra con clic afuera (solo con el boton Cerrar o una accion explicita del formulario) y mantiene un alto FIJO por instancia para que cambiar de pestaña interna (Info/Subtareas/Dependencias/Comentarios/ Documentos) no reajuste su tamaño. De paso corrige un bug real de stacking: ContentDialog no usaba portal a document.body, así que un dialogo de confirmacion abierto desde dentro de un DetailPopup quedaba oculto detras de este. Ahora ambos son portales con una jerarquia de z-index explicita (Flyout 100 < DetailPopup 200 < ContentDialog 250 < Toast 300). En el detalle de actividad, Subtareas y Dependencias pasan de estar apiladas dentro de la pestaña Info a ser cada una su propia pestaña. Regla documentada en project-md-templates/UI-UX-WINDOWS.md (componente DetailPopup) para que aplique a futuros proyectos de escritorio Windows del grupo. Co-Authored-By: Claude Sonnet 5 --- docs/UI-UX-WINDOWS.md | 59 +++++++++++++--- frontend/src/components/ContentDialog.jsx | 11 ++- frontend/src/components/DetailPopup.jsx | 25 +++++++ .../src/pages/Configuracion/CatalogEditor.jsx | 55 ++++++++------- .../pages/Organizacion/DepartmentsPanel.jsx | 40 +++++------ .../src/pages/Organizacion/UsersPanel.jsx | 64 +++++++++-------- .../pages/Proyectos/ActivityDetailPanel.jsx | 69 +++++++++---------- .../pages/Proyectos/ProjectActivitiesTab.jsx | 10 +-- .../src/pages/Proyectos/ProyectosPage.jsx | 8 +-- frontend/src/styles/app.css | 25 ++++++- 10 files changed, 224 insertions(+), 142 deletions(-) create mode 100644 frontend/src/components/DetailPopup.jsx diff --git a/docs/UI-UX-WINDOWS.md b/docs/UI-UX-WINDOWS.md index dde6d0b..e0b77ea 100644 --- a/docs/UI-UX-WINDOWS.md +++ b/docs/UI-UX-WINDOWS.md @@ -118,10 +118,41 @@ openDialog({ **selección múltiple** (checkbox), fila con **hover** y **estado seleccionado** (`accent/.14`), y **clic derecho** que abre el menú contextual de esa fila (ver §6). Alternativa por fila sin mouse derecho: botón `⋯` que abre el mismo menú. -- **Master-Detail (Split View)** — patrón **nativo de Windows** (Correo, Configuración, Explorador): - lista angosta a la izquierda + panel de detalle a la derecha que se actualiza **en el sitio** al - seleccionar un ítem, sin modal. Preferir sobre "detalle en modal" cuando la pantalla es - principalmente de exploración/lectura de una lista larga. +- **DetailPopup** — patrón **por defecto** para ver/crear/editar el detalle de un ítem de un listado + de gestión (proyectos, actividades, usuarios, catálogos, etc.). Overlay que cubre la ventana y deja + **inactivo el listado detrás** (dim + sin interacción) mientras se usa el popup — a diferencia de un + panel lateral o de un panel apilado debajo del listado, el detalle NUNCA reduce ni empuja el + listado, y siempre aparece completo y en el mismo lugar sin importar el largo de la lista ni la + fila elegida. Reglas duras: + - **Nunca se cierra con clic afuera** (a diferencia de `Flyout`) — solo con el botón "Cerrar" de su + cabecera o una acción explícita del formulario (`Guardar`/`Cancelar`). + - **Alto FIJO por instancia**, no se ajusta al contenido: si el popup tiene pestañas o secciones + internas (p. ej. Info/Comentarios/Documentos), cambiar entre ellas **nunca** cambia el tamaño del + popup — solo el contenido interno hace scroll. El ancho/alto se elige según cuánto contenido + espera esa pantalla (un formulario simple de 2 campos no necesita el mismo alto que un detalle + con subtareas y dependencias), pero una vez abierto queda fijo. + - Header propio: título a la izquierda, botón "Cerrar" (ícono `X`) a la derecha. + - Contrato (React; adaptar al framework del proyecto): + `{contenido}` — portal a + `document.body` (igual que `Flyout`/`ContentDialog`), **nunca** anidado dentro del árbol de la + página, para no quedar atrapado en el stacking context de un ancestro y terminar tapado por otro + overlay (bug real: sin portal, un `ContentDialog` abierto DESDE un `DetailPopup` quedaba oculto + detrás de este). Jerarquía de `z-index` entre overlays: `Flyout` (100) < `DetailPopup` (200) < + `ContentDialog` (250) < `Toast` (300) — así una confirmación abierta desde dentro de un + `DetailPopup` siempre queda por encima. + - Secciones internas relacionadas con el ítem (ej. una actividad con Info + Subtareas + + Dependencias + Comentarios + Documentos) van cada una en **su propia pestaña** dentro del popup, + nunca todas apiladas dentro de la pestaña "Info" — mismo criterio que pestañas reales a nivel de + pantalla completa (ver identidad web `UI-UX-LINKEDCORP.md`), aplicado ahora dentro del popup. +- **Master-Detail (Split View)** — patrón **nativo de Windows** (Correo, Explorador): lista angosta a + la izquierda + panel de detalle a la derecha que se actualiza **en el sitio** al seleccionar un + ítem, sin modal. Uso **restringido**: solo para pantallas cuya función principal es explorar/leer + una lista larga como actividad central de esa vista (no para listados de gestión con acciones + crear/editar — ahí va `DetailPopup`). Evitar cualquier variante que ubique el detalle "a un costado" + o "debajo" de un listado de gestión (panel lateral fijo, panel acoplado al borde, panel apilado + bajo la tabla): en la práctica el detalle se siente secundario y el listado pierde protagonismo o + queda "lejos" del detalle cuando la lista crece. Ante la duda entre Master-Detail y `DetailPopup` + para un listado de gestión, usar `DetailPopup`. - **Menú contextual (clic derecho)** — el patrón de interacción **más característico de Windows**: ítems con ícono + label + acelerador de teclado a la derecha (`Ctrl+E`, `Del`…), separadores, submenú con flecha `▸` que se expande al pasar el mouse, ítem destructivo en `danger`. Debe estar @@ -226,11 +257,14 @@ Alt+Tab); si es una decisión puntual ligada al contexto actual, es un `ContentD 1. **CommandBar** (`Nuevo`, `Actualizar`, acciones contextuales + overflow `···`). 2. **Elegir el patrón de datos** según el caso: + - **Listado + `DetailPopup`** (default para listados de gestión: proyectos, actividades, usuarios, + catálogos…): al hacer clic en una fila u optar "Nuevo", se abre un `DetailPopup` (overlay de + tamaño fijo, listado inactivo detrás, nunca se cierra con clic afuera) con el detalle/formulario + y sus acciones (`Guardar`, `Editar`, `Eliminar`). El listado se ve completo e intacto en cuanto + se cierra el popup. - **Listado + Master-Detail**: lista con selección simple a la izquierda/arriba, detalle **en el - sitio** a la derecha/abajo. Preferido cuando el usuario navega/lee mucho (correo, catálogos). - - **Listado + ContentDialog de detalle**: al hacer clic en una fila se abre un `ContentDialog` - (o una ventana secundaria si el contenido es largo) con el detalle y acciones (`Editar`, - `Eliminar`). Preferido cuando el detalle es ocasional y la lista debe verse completa mientras se usa. + sitio** a la derecha/abajo, sin modal. Reservado para pantallas cuya función principal es + explorar/leer una lista larga (correo) — no para listados de gestión con crear/editar. 3. **Selección múltiple + acciones por lote**: casillas en la columna izquierda de la tabla; la barra de estado muestra "`N` de `M` seleccionados"; las acciones de lote aparecen en la `CommandBar` solo cuando hay selección. @@ -261,11 +295,16 @@ Alt+Tab); si es una decisión puntual ligada al contexto actual, es un `ContentD con el resto de Windows. - ✅ Radios de esquina consistentes: `4px` controles, `8px` superficies elevadas y la ventana misma. - ✅ Un solo set de íconos en toda la app (Lucide o Segoe Fluent Icons), heredando color del contexto. -- ✅ Master-Detail para listas de exploración; `ContentDialog`/ventana secundaria para detalle ocasional. +- ✅ `DetailPopup` (tamaño fijo, nunca se cierra con clic afuera) para ver/crear/editar el detalle de + un ítem de un listado de gestión; Master-Detail solo para pantallas de exploración/lectura pura. - ✅ Foco visible solo con teclado (`:focus-visible`); hover visible siempre (mouse-first). - ❌ `MessageBox`/`alert`/`confirm` del sistema. ❌ combos/selects/calendarios nativos sin estilar. ❌ mezclar sets de íconos. ❌ apilar listado + formulario + detalle en una sola pantalla sin - separación clara. ❌ usar `Toast` para pedir una decisión al usuario. + separación clara. ❌ usar `Toast` para pedir una decisión al usuario. ❌ panel de detalle lateral o + debajo del listado en pantallas de gestión (usar `DetailPopup`). ❌ que un `DetailPopup` cambie de + tamaño al moverse entre sus pestañas/secciones internas. ❌ montar `ContentDialog`/`DetailPopup` sin + portal a `document.body` (queda atrapado en el stacking context de un ancestro y puede terminar + tapado por otro overlay). > Para que una app "se sienta Windows moderna": misma barra de título, mismo panel de navegación, > mismos cuatro mecanismos de aviso (`ContentDialog`/`InfoBar`/`Toast`/`Flyout`) bien diferenciados, diff --git a/frontend/src/components/ContentDialog.jsx b/frontend/src/components/ContentDialog.jsx index 4795452..4e53f26 100644 --- a/frontend/src/components/ContentDialog.jsx +++ b/frontend/src/components/ContentDialog.jsx @@ -1,3 +1,4 @@ +import { createPortal } from 'react-dom'; import { useUi } from '../context/ui-context.jsx'; import { Button } from './Button.jsx'; @@ -17,8 +18,11 @@ export function ContentDialogHost() { if (!action.keepOpen) closeDialog(); } - return ( -
{ if (e.target === e.currentTarget) closeDialog(); }}> + // Regla dura: NUNCA se cierra con clic afuera (a diferencia de Flyout) — solo con un boton. + // Portal a document.body (igual que Flyout/DetailPopup): garantiza que quede SIEMPRE por + // encima de cualquier DetailPopup abierto, sin depender del punto del arbol donde se monta. + return createPortal( +
@@ -33,6 +37,7 @@ export function ContentDialogHost() { ))}
-
+
, + document.body ); } diff --git a/frontend/src/components/DetailPopup.jsx b/frontend/src/components/DetailPopup.jsx new file mode 100644 index 0000000..28a0f0c --- /dev/null +++ b/frontend/src/components/DetailPopup.jsx @@ -0,0 +1,25 @@ +import { createPortal } from 'react-dom'; +import { X } from 'lucide-react'; + +// Popup grande para ver/editar el detalle de un elemento de un listado (reemplaza el patron +// "panel debajo del listado"). Reglas duras: +// 1. NUNCA se cierra con clic afuera del popup — solo con el boton "Cerrar" de su header o una +// accion explicita del formulario (ej. Guardar/Cancelar). +// 2. Alto FIJO (no se ajusta al contenido): moverse entre pestañas/secciones internas del popup +// (ej. Info/Comentarios/Documentos) nunca debe cambiar su tamaño — solo el contenido interno +// hace scroll. `height` es fijo por instancia; ajustar via prop segun cuanto contenido espera. +export function DetailPopup({ open, title, onClose, children, width = 720, height = 640 }) { + if (!open) return null; + return createPortal( +
+
+
+
{title}
+ +
+
{children}
+
+
, + document.body + ); +} diff --git a/frontend/src/pages/Configuracion/CatalogEditor.jsx b/frontend/src/pages/Configuracion/CatalogEditor.jsx index 816e9fd..839c44e 100644 --- a/frontend/src/pages/Configuracion/CatalogEditor.jsx +++ b/frontend/src/pages/Configuracion/CatalogEditor.jsx @@ -8,6 +8,7 @@ import { Button } from '../../components/Button.jsx'; import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { Badge } from '../../components/Badge.jsx'; +import { DetailPopup } from '../../components/DetailPopup.jsx'; const COLOR_OPTIONS = ['accent', 'success', 'warning', 'danger', 'muted'].map((c) => ({ value: c, label: c })); @@ -93,35 +94,33 @@ export function CatalogEditor({ catalogKey, fields, title }) { ]} /> - {editing && ( -
- - {fields.map((f) => ( - - {f.type === 'color' ? ( - setForm((s) => ({ ...s, [f.key]: v }))} /> - ) : f.type === 'checkbox' ? ( - - ) : ( - setForm((s) => ({ ...s, [f.key]: f.type === 'number' ? Number(e.target.value) : e.target.value }))} - /> - )} - - ))} - -
- - -
+ setEditing(false)} width={480} height={380}> + + {fields.map((f) => ( + + {f.type === 'color' ? ( + setForm((s) => ({ ...s, [f.key]: v }))} /> + ) : f.type === 'checkbox' ? ( + + ) : ( + setForm((s) => ({ ...s, [f.key]: f.type === 'number' ? Number(e.target.value) : e.target.value }))} + /> + )} + + ))} + +
+ +
- )} +
); } diff --git a/frontend/src/pages/Organizacion/DepartmentsPanel.jsx b/frontend/src/pages/Organizacion/DepartmentsPanel.jsx index 8ac78b5..ce14d70 100644 --- a/frontend/src/pages/Organizacion/DepartmentsPanel.jsx +++ b/frontend/src/pages/Organizacion/DepartmentsPanel.jsx @@ -7,6 +7,7 @@ import { Treeview } from '../../components/Treeview.jsx'; import { Button } from '../../components/Button.jsx'; import { Field } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; +import { DetailPopup } from '../../components/DetailPopup.jsx'; function flatten(nodes, acc = []) { for (const n of nodes) { @@ -73,8 +74,8 @@ export default function DepartmentsPanel() { if (loading || !tree) return
{t('common_loading')}
; return ( -
-
+
+

{t('org_departments')}

@@ -82,26 +83,23 @@ export default function DepartmentsPanel() {
- {editing && ( -
-

{selected ? t('common_edit') : t('department_new')}

- - setForm((f) => ({ ...f, name: e.target.value }))} autoFocus /> - - - o.value !== selected?.id)]} - value={form.parentId} - onChange={(v) => setForm((f) => ({ ...f, parentId: v }))} - /> - -
- - - {selected && } -
+ setEditing(false)} width={480} height={360}> + + setForm((f) => ({ ...f, name: e.target.value }))} autoFocus /> + + + o.value !== selected?.id)]} + value={form.parentId} + onChange={(v) => setForm((f) => ({ ...f, parentId: v }))} + /> + +
+ + + {selected && }
- )} +
); } diff --git a/frontend/src/pages/Organizacion/UsersPanel.jsx b/frontend/src/pages/Organizacion/UsersPanel.jsx index 07d3be4..fe2ea3a 100644 --- a/frontend/src/pages/Organizacion/UsersPanel.jsx +++ b/frontend/src/pages/Organizacion/UsersPanel.jsx @@ -10,6 +10,7 @@ import { Button } from '../../components/Button.jsx'; import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { Badge } from '../../components/Badge.jsx'; +import { DetailPopup } from '../../components/DetailPopup.jsx'; function flatten(nodes, acc = []) { for (const n of nodes) { acc.push(n); if (n.children?.length) flatten(n.children, acc); } @@ -103,41 +104,38 @@ export default function UsersPanel() { ]} /> - {editing && ( -
-

{selected ? t('common_edit') : t('user_new')}

- - - setForm((f) => ({ ...f, name: e.target.value }))} autoFocus /> + setEditing(false)} width={560} height={440}> + + + setForm((f) => ({ ...f, name: e.target.value }))} autoFocus /> + + + setForm((f) => ({ ...f, email: e.target.value }))} /> + + + + + setForm((f) => ({ ...f, roleId: v }))} /> + + + setForm((f) => ({ ...f, departmentId: v }))} /> + + + + + setForm((f) => ({ ...f, position: e.target.value }))} /> + + {!selected && ( + + setForm((f) => ({ ...f, password: e.target.value }))} /> - - setForm((f) => ({ ...f, email: e.target.value }))} /> - - - - - setForm((f) => ({ ...f, roleId: v }))} /> - - - setForm((f) => ({ ...f, departmentId: v }))} /> - - - - - setForm((f) => ({ ...f, position: e.target.value }))} /> - - {!selected && ( - - setForm((f) => ({ ...f, password: e.target.value }))} /> - - )} - -
- - -
+ )} +
+
+ +
- )} +
); } diff --git a/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx b/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx index 79674ec..28f78cd 100644 --- a/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx +++ b/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx @@ -23,7 +23,7 @@ function toForm(a) { }; } -export default function ActivityDetailPanel({ activity, projectId, siblingActivities, onChanged, onClose }) { +export default function ActivityDetailPanel({ activity, projectId, siblingActivities, onChanged }) { const { t, pushToast } = useUi(); const runAction = useAction(); const { data: priorities } = useAsync(() => listCatalog('priorities'), []); @@ -84,15 +84,12 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi } return ( -
-
-

{activity.code} — {activity.name}

- -
- +
- {hasSubtasks && {t('subtasks_title')}}
) : ( @@ -179,38 +175,41 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
)} +
+ )} -
-

{t('subtasks_title')}

- {(subtasks || []).map((s) => ( -
- toggleSubtask(s)} /> - {s.name} - -
- ))} -
- setNewSubtask(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addSubtaskItem()} /> - + {tab === 'subtasks' && ( +
+ {(subtasks || []).map((s) => ( +
+ toggleSubtask(s)} /> + {s.name} +
+ ))} + {(!subtasks || subtasks.length === 0) &&
{t('common_empty')}
} +
+ setNewSubtask(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addSubtaskItem()} /> +
+
+ )} -
-

{t('activity_dependencies')}

- {(activity.dependencies || []).length === 0 &&
{t('common_empty')}
} - {(activity.dependencies || []).map((d) => ( -
- - {d.code} — {d.name} - -
- ))} -
-
- -
- + {tab === 'dependencies' && ( +
+ {(activity.dependencies || []).length === 0 &&
{t('common_empty')}
} + {(activity.dependencies || []).map((d) => ( +
+ + {d.code} — {d.name} +
+ ))} +
+
+ +
+
)} diff --git a/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx b/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx index e193891..c7b9aae 100644 --- a/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx +++ b/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx @@ -12,6 +12,7 @@ import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { StatusBadge, ProgressBar, DueBadge } from '../../components/StatusBadge.jsx'; import { dueStatus } from '../../utils/dueStatus.js'; +import { DetailPopup } from '../../components/DetailPopup.jsx'; import ActivityDetailPanel from './ActivityDetailPanel.jsx'; const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityId: '', statusId: '', startDate: '', dueDate: '', isMilestone: false }; @@ -128,17 +129,16 @@ export default function ProjectActivitiesTab({ projectId }) {
)} - {selected && ( -
+ setSelected(null)} width={720} height={680}> + {selected && ( setSelected(null)} /> -
- )} + )} +
); } diff --git a/frontend/src/pages/Proyectos/ProyectosPage.jsx b/frontend/src/pages/Proyectos/ProyectosPage.jsx index 288bf56..f2e7e1a 100644 --- a/frontend/src/pages/Proyectos/ProyectosPage.jsx +++ b/frontend/src/pages/Proyectos/ProyectosPage.jsx @@ -13,6 +13,7 @@ import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { StatusBadge, ProgressBar } from '../../components/StatusBadge.jsx'; import { Badge } from '../../components/Badge.jsx'; +import { DetailPopup } from '../../components/DetailPopup.jsx'; function flatten(nodes, acc = []) { for (const n of nodes) { acc.push(n); if (n.children?.length) flatten(n.children, acc); } @@ -98,9 +99,7 @@ export default function ProyectosPage() { onRowClick={(p) => navigate(`/proyectos/${p.id}`)} /> - {creating && ( -
-

{t('project_new')}

+ setCreating(false)} width={720} height={720}> setForm((f) => ({ ...f, name: e.target.value }))} autoFocus /> @@ -147,8 +146,7 @@ export default function ProyectosPage() {
-
- )} +
); } diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index ac98c3c..4742185 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -130,7 +130,8 @@ button { font-family: inherit; } table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; } .list-table th { text-align: left; padding: 10px 12px; font-size: .7rem; text-transform: uppercase; letter-spacing: .03em; - color: rgb(var(--muted)); border-bottom: 1px solid rgb(var(--line)); cursor: pointer; user-select: none; + color: rgb(var(--muted)); border-bottom: 1px solid rgb(var(--line)); cursor: pointer; + -webkit-user-select: none; user-select: none; white-space: nowrap; } .list-table th:hover { color: rgb(var(--body)); } @@ -174,7 +175,7 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; } /* --- ContentDialog --- */ .dialog-overlay { position: fixed; inset: 0; background: rgb(0 0 0 / .5); - display: flex; align-items: center; justify-content: center; z-index: 200; padding: 16px; + display: flex; align-items: center; justify-content: center; z-index: 250; padding: 16px; } .dialog-surface { width: 420px; max-width: 100%; background: rgb(var(--surface)); border-radius: var(--r-lg); @@ -258,6 +259,26 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; } .login-title { font-size: 1.1rem; font-weight: 700; } .login-error { color: rgb(var(--danger)); font-size: .78rem; margin-bottom: 10px; } +/* --- DetailPopup (ver DetailPopup.jsx) --- + Al elegir un item de un listado, la lista queda inactiva detras del overlay y el popup + presenta la pantalla de info/edicion completa. Regla dura: NUNCA se cierra con clic afuera + (a diferencia de Flyout); solo con el boton "Cerrar" o una accion explicita del formulario. */ +.detail-popup-overlay { + position: fixed; inset: 0; background: rgb(0 0 0 / .55); + display: flex; align-items: center; justify-content: center; z-index: 200; padding: 24px; +} +.detail-popup { + width: 100%; background: rgb(var(--surface)); border-radius: var(--r-lg); + box-shadow: 0 24px 64px rgb(0 0 0 / .45); + max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; +} +.detail-popup-header { + display: flex; align-items: center; justify-content: space-between; gap: 10px; + padding: 16px 20px; border-bottom: 1px solid rgb(var(--line)); flex: none; +} +.detail-popup-title { font-weight: 700; font-size: 1rem; } +.detail-popup-body { padding: 20px; overflow-y: auto; flex: 1; } + /* --- Expander (Configuracion) --- */ .expander { border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); margin-bottom: 10px; overflow: hidden; } .expander-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; cursor: pointer; background: rgb(var(--surface)); }