fix(ui): reemplaza panel-debajo-del-listado por DetailPopup de tamaño fijo
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="dialog-overlay" onMouseDown={(e) => { 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(
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog-surface">
|
||||
<div className="dialog-stripe" style={{ background: `rgb(var(--${STRIPE_COLOR[tone]}))` }} />
|
||||
<div className="dialog-body">
|
||||
@@ -33,6 +37,7 @@ export function ContentDialogHost() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<div className="detail-popup-overlay">
|
||||
<div className="detail-popup" style={{ maxWidth: width, height }}>
|
||||
<div className="detail-popup-header">
|
||||
<div className="detail-popup-title">{title}</div>
|
||||
<button className="icon-btn" onClick={onClose}><X /></button>
|
||||
</div>
|
||||
<div className="detail-popup-body">{children}</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
<div className="panel-card" style={{ marginTop: 12, maxWidth: 480 }}>
|
||||
<FieldRow>
|
||||
{fields.map((f) => (
|
||||
<Field key={f.key} label={f.label}>
|
||||
{f.type === 'color' ? (
|
||||
<ComboBox options={COLOR_OPTIONS} value={form[f.key]} onChange={(v) => setForm((s) => ({ ...s, [f.key]: v }))} />
|
||||
) : f.type === 'checkbox' ? (
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={!!form[f.key]} onChange={(e) => setForm((s) => ({ ...s, [f.key]: e.target.checked }))} />
|
||||
{f.label}
|
||||
</label>
|
||||
) : (
|
||||
<input
|
||||
className="input"
|
||||
type={f.type === 'number' ? 'number' : 'text'}
|
||||
value={form[f.key]}
|
||||
onChange={(e) => setForm((s) => ({ ...s, [f.key]: f.type === 'number' ? Number(e.target.value) : e.target.value }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
</FieldRow>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button size="sm" variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button size="sm" variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
<DetailPopup open={editing} title={selected ? t('common_edit') : t('common_new')} onClose={() => setEditing(false)} width={480} height={380}>
|
||||
<FieldRow>
|
||||
{fields.map((f) => (
|
||||
<Field key={f.key} label={f.label}>
|
||||
{f.type === 'color' ? (
|
||||
<ComboBox options={COLOR_OPTIONS} value={form[f.key]} onChange={(v) => setForm((s) => ({ ...s, [f.key]: v }))} />
|
||||
) : f.type === 'checkbox' ? (
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={!!form[f.key]} onChange={(e) => setForm((s) => ({ ...s, [f.key]: e.target.checked }))} />
|
||||
{f.label}
|
||||
</label>
|
||||
) : (
|
||||
<input
|
||||
className="input"
|
||||
type={f.type === 'number' ? 'number' : 'text'}
|
||||
value={form[f.key]}
|
||||
onChange={(e) => setForm((s) => ({ ...s, [f.key]: f.type === 'number' ? Number(e.target.value) : e.target.value }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
</FieldRow>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button size="sm" variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button size="sm" variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</DetailPopup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <div className="text-muted">{t('common_loading')}</div>;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<div className="panel-card" style={{ flex: '1 1 280px' }}>
|
||||
<div>
|
||||
<div className="panel-card">
|
||||
<div className="toolbar">
|
||||
<h3 style={{ margin: 0 }}>{t('org_departments')}</h3>
|
||||
<Button variant="accent" icon={Plus} onClick={startNew}>{t('department_new')}</Button>
|
||||
@@ -82,26 +83,23 @@ export default function DepartmentsPanel() {
|
||||
<Treeview nodes={tree} onSelect={startEdit} selectedId={selected?.id} />
|
||||
</div>
|
||||
|
||||
{editing && (
|
||||
<div className="panel-card" style={{ flex: '1 1 280px', maxWidth: 360 }}>
|
||||
<h3>{selected ? t('common_edit') : t('department_new')}</h3>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
</Field>
|
||||
<Field label={t('department_parent')}>
|
||||
<ComboBox
|
||||
options={[{ value: '', label: '—' }, ...flatOptions.filter((o) => o.value !== selected?.id)]}
|
||||
value={form.parentId}
|
||||
onChange={(v) => setForm((f) => ({ ...f, parentId: v }))}
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
|
||||
{selected && <Button variant="danger" icon={Trash2} onClick={() => confirmDelete(selected)}>{t('common_delete')}</Button>}
|
||||
</div>
|
||||
<DetailPopup open={editing} title={selected ? t('common_edit') : t('department_new')} onClose={() => setEditing(false)} width={480} height={360}>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
</Field>
|
||||
<Field label={t('department_parent')}>
|
||||
<ComboBox
|
||||
options={[{ value: '', label: '—' }, ...flatOptions.filter((o) => o.value !== selected?.id)]}
|
||||
value={form.parentId}
|
||||
onChange={(v) => setForm((f) => ({ ...f, parentId: v }))}
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
|
||||
{selected && <Button variant="danger" icon={Trash2} onClick={() => confirmDelete(selected)}>{t('common_delete')}</Button>}
|
||||
</div>
|
||||
)}
|
||||
</DetailPopup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<div className="panel-card" style={{ marginTop: 16, maxWidth: 520 }}>
|
||||
<h3>{selected ? t('common_edit') : t('user_new')}</h3>
|
||||
<FieldRow>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
<DetailPopup open={editing} title={selected ? t('common_edit') : t('user_new')} onClose={() => setEditing(false)} width={560} height={440}>
|
||||
<FieldRow>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
</Field>
|
||||
<Field label={t('user_email')}>
|
||||
<input className="input" type="email" value={form.email} disabled={!!selected} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('user_role')}>
|
||||
<ComboBox options={roleOptions} value={form.roleId} onChange={(v) => setForm((f) => ({ ...f, roleId: v }))} />
|
||||
</Field>
|
||||
<Field label={t('common_department')}>
|
||||
<ComboBox options={[{ value: '', label: '—' }, ...deptOptions]} value={form.departmentId} onChange={(v) => setForm((f) => ({ ...f, departmentId: v }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('user_position')}>
|
||||
<input className="input" value={form.position} onChange={(e) => setForm((f) => ({ ...f, position: e.target.value }))} />
|
||||
</Field>
|
||||
{!selected && (
|
||||
<Field label={t('user_password')}>
|
||||
<input className="input" type="password" value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} />
|
||||
</Field>
|
||||
<Field label={t('user_email')}>
|
||||
<input className="input" type="email" value={form.email} disabled={!!selected} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('user_role')}>
|
||||
<ComboBox options={roleOptions} value={form.roleId} onChange={(v) => setForm((f) => ({ ...f, roleId: v }))} />
|
||||
</Field>
|
||||
<Field label={t('common_department')}>
|
||||
<ComboBox options={[{ value: '', label: '—' }, ...deptOptions]} value={form.departmentId} onChange={(v) => setForm((f) => ({ ...f, departmentId: v }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('user_position')}>
|
||||
<input className="input" value={form.position} onChange={(e) => setForm((f) => ({ ...f, position: e.target.value }))} />
|
||||
</Field>
|
||||
{!selected && (
|
||||
<Field label={t('user_password')}>
|
||||
<input className="input" type="password" value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} />
|
||||
</Field>
|
||||
)}
|
||||
</FieldRow>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</FieldRow>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</DetailPopup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="panel-card">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<h3 style={{ margin: 0 }}>{activity.code} — {activity.name}</h3>
|
||||
<Button variant="subtle" icon={X} onClick={onClose}>{t('common_close')}</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ key: 'detail', label: t('tab_info') },
|
||||
{ key: 'subtasks', label: t('subtasks_title') },
|
||||
{ key: 'dependencies', label: t('activity_dependencies') },
|
||||
{ key: 'comments', label: t('tab_comments') },
|
||||
{ key: 'documents', label: t('tab_documents') },
|
||||
]}
|
||||
@@ -128,7 +125,6 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
||||
</FieldRow>
|
||||
<Field label={t('activity_progress')}>
|
||||
<ProgressBar value={Number(activity.progress_pct)} />
|
||||
{hasSubtasks && <span className="text-muted" style={{ fontSize: '.68rem' }}>{t('subtasks_title')}</span>}
|
||||
</Field>
|
||||
</div>
|
||||
) : (
|
||||
@@ -179,38 +175,41 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
||||
</FieldRow>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<h4>{t('subtasks_title')}</h4>
|
||||
{(subtasks || []).map((s) => (
|
||||
<div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }}>
|
||||
<input type="checkbox" checked={!!s.is_done} onChange={() => toggleSubtask(s)} />
|
||||
<span style={{ flex: 1, fontSize: '.8rem', textDecoration: s.is_done ? 'line-through' : 'none', opacity: s.is_done ? .6 : 1 }}>{s.name}</span>
|
||||
<button className="icon-btn" onClick={() => removeSubtaskItem(s)}><Trash2 size={15} /></button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<input className="input" value={newSubtask} placeholder={t('subtask_new')} onChange={(e) => setNewSubtask(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addSubtaskItem()} />
|
||||
<Button variant="standard" icon={Plus} onClick={addSubtaskItem}>{t('common_new')}</Button>
|
||||
{tab === 'subtasks' && (
|
||||
<div>
|
||||
{(subtasks || []).map((s) => (
|
||||
<div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }}>
|
||||
<input type="checkbox" checked={!!s.is_done} onChange={() => toggleSubtask(s)} />
|
||||
<span style={{ flex: 1, fontSize: '.8rem', textDecoration: s.is_done ? 'line-through' : 'none', opacity: s.is_done ? .6 : 1 }}>{s.name}</span>
|
||||
<button className="icon-btn" onClick={() => removeSubtaskItem(s)}><Trash2 size={15} /></button>
|
||||
</div>
|
||||
))}
|
||||
{(!subtasks || subtasks.length === 0) && <div className="text-muted" style={{ fontSize: '.78rem' }}>{t('common_empty')}</div>}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<input className="input" value={newSubtask} placeholder={t('subtask_new')} onChange={(e) => setNewSubtask(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addSubtaskItem()} />
|
||||
<Button variant="standard" icon={Plus} onClick={addSubtaskItem}>{t('common_new')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<h4>{t('activity_dependencies')}</h4>
|
||||
{(activity.dependencies || []).length === 0 && <div className="text-muted" style={{ fontSize: '.78rem' }}>{t('common_empty')}</div>}
|
||||
{(activity.dependencies || []).map((d) => (
|
||||
<div key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }}>
|
||||
<Link2 size={14} style={{ opacity: .6 }} />
|
||||
<span style={{ flex: 1, fontSize: '.8rem' }}>{d.code} — {d.name}</span>
|
||||
<button className="icon-btn" onClick={() => removeDep(d.id)}><Trash2 size={15} /></button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ComboBox options={depOptions} value={addingDep} onChange={setAddingDep} placeholder={t('activity_depends_on')} />
|
||||
</div>
|
||||
<Button variant="standard" icon={Plus} onClick={addDep}>{t('activity_add_dependency')}</Button>
|
||||
{tab === 'dependencies' && (
|
||||
<div>
|
||||
{(activity.dependencies || []).length === 0 && <div className="text-muted" style={{ fontSize: '.78rem' }}>{t('common_empty')}</div>}
|
||||
{(activity.dependencies || []).map((d) => (
|
||||
<div key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }}>
|
||||
<Link2 size={14} style={{ opacity: .6 }} />
|
||||
<span style={{ flex: 1, fontSize: '.8rem' }}>{d.code} — {d.name}</span>
|
||||
<button className="icon-btn" onClick={() => removeDep(d.id)}><Trash2 size={15} /></button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ComboBox options={depOptions} value={addingDep} onChange={setAddingDep} placeholder={t('activity_depends_on')} />
|
||||
</div>
|
||||
<Button variant="standard" icon={Plus} onClick={addDep}>{t('activity_add_dependency')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<DetailPopup open={!!selected} title={selected ? `${selected.code} — ${selected.name}` : ''} onClose={() => setSelected(null)} width={720} height={680}>
|
||||
{selected && (
|
||||
<ActivityDetailPanel
|
||||
activity={selected}
|
||||
projectId={projectId}
|
||||
siblingActivities={activities}
|
||||
onChanged={refreshAll}
|
||||
onClose={() => setSelected(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</DetailPopup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<div className="panel-card" style={{ marginTop: 16 }}>
|
||||
<h3>{t('project_new')}</h3>
|
||||
<DetailPopup open={creating} title={t('project_new')} onClose={() => setCreating(false)} width={720} height={720}>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
</Field>
|
||||
@@ -147,8 +146,7 @@ export default function ProyectosPage() {
|
||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
<Button variant="standard" onClick={() => setCreating(false)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DetailPopup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)); }
|
||||
|
||||
Reference in New Issue
Block a user