1d22adc42e
Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias, dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1266 lines
92 KiB
HTML
1266 lines
92 KiB
HTML
<!doctype html>
|
||
<html lang="es" data-theme="dark" data-accent="blue" data-navmode="expanded">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Identidad UI/UX — Windows (shell nombrado, muestra viva)</title>
|
||
<style>
|
||
/* ====== TOKENS (coinciden con UI-UX-WINDOWS.md) — inspirados en Fluent Design / Windows 11 ====== */
|
||
:root{
|
||
--canvas:32 32 32; --surface:43 43 43; --panel:51 51 51; --field:43 43 43;
|
||
--body:255 255 255; --muted:168 168 168; --line:70 70 70; --line-strong:90 90 90;
|
||
--success:108 203 95; --warning:255 185 0; --danger:255 105 97;
|
||
--app-font:'Segoe UI Variable Text','Segoe UI',system-ui,-apple-system,Roboto,sans-serif;
|
||
--app-font-disp:'Segoe UI Variable Display','Segoe UI Variable Text','Segoe UI',system-ui,sans-serif;
|
||
--r-sm:4px; --r-lg:8px;
|
||
--w-nav:230px; --w-nav-compact:52px; --h-titulo:40px; --h-comandos:48px; --h-migas:36px; --h-estado:26px;
|
||
--shadow-flyout:0 8px 24px rgb(0 0 0/.35),0 0 0 1px rgb(var(--line)/.5);
|
||
--shadow-win:0 24px 70px rgb(0 0 0/.55);
|
||
}
|
||
:root[data-theme="light"]{
|
||
--canvas:243 243 243; --surface:255 255 255; --panel:249 249 249; --field:255 255 255;
|
||
--body:32 32 32; --muted:96 96 96; --line:225 225 225; --line-strong:200 200 200;
|
||
--success:16 124 16; --warning:157 93 0; --danger:196 43 28;
|
||
--shadow-flyout:0 8px 24px rgb(0 0 0/.18),0 0 0 1px rgb(var(--line)/.7);
|
||
--shadow-win:0 24px 70px rgb(0 0 0/.28);
|
||
}
|
||
/* Acentos: paleta de acento de Windows (el usuario del SO elige uno; la app lo hereda) */
|
||
:root[data-accent="blue"]{--accent:0 120 212;--accent-2:0 95 184;}
|
||
:root[data-accent="purple"]{--accent:135 100 184;--accent-2:116 77 169;}
|
||
:root[data-accent="green"]{--accent:16 137 62;--accent-2:12 110 50;}
|
||
:root[data-accent="red"]{--accent:196 43 28;--accent-2:164 34 22;}
|
||
:root[data-accent="orange"]{--accent:202 80 16;--accent-2:170 65 10;}
|
||
:root[data-accent="teal"]{--accent:0 153 148;--accent-2:0 130 125;}
|
||
|
||
*{box-sizing:border-box}
|
||
html,body{height:100%}
|
||
body{margin:0;font-family:var(--app-font);background:
|
||
radial-gradient(1200px 800px at 15% -10%,rgb(var(--accent)/.20),transparent 60%),
|
||
radial-gradient(1000px 700px at 100% 100%,rgb(var(--accent)/.12),transparent 55%),
|
||
rgb(var(--canvas));
|
||
color:rgb(var(--body));line-height:1.5;-webkit-font-smoothing:antialiased;font-size:14px}
|
||
:root[data-theme="light"] body{background:
|
||
radial-gradient(1200px 800px at 15% -10%,rgb(var(--accent)/.10),transparent 60%),
|
||
radial-gradient(1000px 700px at 100% 100%,rgb(var(--accent)/.08),transparent 55%),
|
||
rgb(var(--canvas))}
|
||
.muted{color:rgb(var(--muted))} .xs{font-size:.72rem} .sm{font-size:.82rem}
|
||
h2{font-family:var(--app-font-disp);font-size:1.25rem;font-weight:600;margin:0 0 .2rem}
|
||
h3{font-size:.95rem;font-weight:600;margin:0}
|
||
p{margin:0 0 .6rem}
|
||
a{color:rgb(var(--accent))}
|
||
::-webkit-scrollbar{width:10px;height:10px} ::-webkit-scrollbar-thumb{background:rgb(var(--line-strong));border-radius:99px;border:2px solid transparent;background-clip:padding-box}
|
||
::-webkit-scrollbar-track{background:transparent}
|
||
:focus{outline:none}
|
||
:focus-visible{outline:2px solid rgb(var(--accent));outline-offset:1px;border-radius:var(--r-sm)}
|
||
|
||
/* ====== DESKTOP + VENTANA (simula una app de escritorio flotando sobre el escritorio) ====== */
|
||
.desktop{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2.2vh 1.5vw}
|
||
.ventana{width:min(1440px,97vw);height:min(880px,95vh);background:rgb(var(--surface));border-radius:var(--r-lg);
|
||
box-shadow:var(--shadow-win);border:1px solid rgb(var(--line));overflow:hidden;display:flex;flex-direction:column}
|
||
|
||
/* barra-titulo: reemplaza la barra nativa de Windows; arrastrable salvo sobre controles */
|
||
.barra-titulo{height:var(--h-titulo);flex:none;display:flex;align-items:center;gap:.5rem;
|
||
padding:0 0 0 .7rem;background:rgb(var(--surface));border-bottom:1px solid rgb(var(--line));
|
||
-webkit-app-region:drag;user-select:none}
|
||
.barra-titulo .marca-app{display:flex;align-items:center;gap:.5rem;font-size:.8rem;font-weight:600;color:rgb(var(--muted))}
|
||
.barra-titulo .marca-app .ico{width:16px;height:16px;color:rgb(var(--accent))}
|
||
.barra-titulo .acciones-titulo{margin-left:auto;display:flex;height:100%;-webkit-app-region:no-drag}
|
||
.btn-win{width:46px;height:100%;border:0;background:transparent;color:rgb(var(--body));display:grid;place-items:center;cursor:pointer}
|
||
.btn-win svg{width:10px;height:10px}
|
||
.btn-win:hover{background:rgb(var(--panel))}
|
||
.btn-win.cerrar:hover{background:#c42b1c;color:#fff}
|
||
.btn-win.maximizar{position:relative}
|
||
.snap-preview{position:absolute;top:100%;right:0;background:rgb(var(--surface));border:1px solid rgb(var(--line));
|
||
border-radius:var(--r-lg);box-shadow:var(--shadow-flyout);padding:.5rem;display:none;gap:.4rem;z-index:500;-webkit-app-region:no-drag}
|
||
.btn-win.maximizar:hover .snap-preview{display:flex}
|
||
.snap-opt{width:46px;height:34px;border:1px solid rgb(var(--line-strong));border-radius:3px;background:rgb(var(--panel));display:grid;padding:2px;gap:2px;cursor:pointer}
|
||
.snap-opt:hover{border-color:rgb(var(--accent))}
|
||
.snap-opt.s2{grid-template-columns:1fr 1fr}
|
||
.snap-opt.s2 i{background:rgb(var(--accent)/.5);border-radius:1px}
|
||
.snap-opt.s3{grid-template-columns:1.3fr 1fr}
|
||
.snap-opt.s3 i{background:rgb(var(--accent)/.5);border-radius:1px}
|
||
.snap-opt.s4{grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr}
|
||
.snap-opt.s4 i{background:rgb(var(--accent)/.5);border-radius:1px}
|
||
|
||
/* ====== APP-SHELL ====== */
|
||
.app-shell{flex:1;min-height:0;display:flex;overflow:hidden}
|
||
|
||
/* navegacion (NavigationView): expandida / compacta / oculta */
|
||
.navegacion{width:var(--w-nav);flex:none;height:100%;overflow-y:auto;overflow-x:hidden;
|
||
background:rgb(var(--panel)/.5);border-right:1px solid rgb(var(--line));padding:.6rem;
|
||
display:flex;flex-direction:column;gap:.15rem;transition:width .16s ease;z-index:20}
|
||
html[data-navmode="compact"] .navegacion{width:var(--w-nav-compact)}
|
||
html[data-navmode="oculto"] .navegacion{width:0;padding:0;border:0;overflow:hidden}
|
||
.nav-top{display:flex;align-items:center;gap:.3rem;margin-bottom:.4rem}
|
||
.nav-toggle{width:36px;height:36px;flex:none;border:0;background:transparent;border-radius:var(--r-sm);color:rgb(var(--body));display:grid;place-items:center;cursor:pointer}
|
||
.nav-toggle:hover{background:rgb(var(--panel))}
|
||
.nav-toggle svg{width:16px;height:16px}
|
||
.nav-buscar{flex:1;min-width:0}
|
||
html[data-navmode="compact"] .nav-buscar,html[data-navmode="compact"] .nav-search-label{display:none}
|
||
.nav-search{width:100%;height:32px;border-radius:var(--r-sm);border:1px solid rgb(var(--line));background:rgb(var(--field));
|
||
color:rgb(var(--body));font-size:.78rem;padding:0 .6rem}
|
||
.nav-search:focus{border-color:rgb(var(--accent))}
|
||
.item-nav{position:relative;display:flex;align-items:center;gap:.65rem;height:36px;padding:0 .6rem;border-radius:var(--r-sm);
|
||
color:rgb(var(--muted));text-decoration:none;font-size:.82rem;cursor:pointer;white-space:nowrap}
|
||
.item-nav .ico{width:16px;height:16px;flex:none}
|
||
html[data-navmode="compact"] .item-nav{justify-content:center;padding:0}
|
||
html[data-navmode="compact"] .item-nav span.lbl{display:none}
|
||
.item-nav:hover{background:rgb(var(--panel))}
|
||
.item-nav.active{background:rgb(var(--panel));color:rgb(var(--body));font-weight:600}
|
||
.item-nav.active::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:99px;background:rgb(var(--accent))}
|
||
.nav-sep{height:1px;background:rgb(var(--line));margin:.5rem .2rem}
|
||
.pie-navegacion{margin-top:auto;display:flex;flex-direction:column;gap:.15rem}
|
||
.cuenta-nav{display:flex;align-items:center;gap:.6rem;height:44px;padding:0 .5rem;border-radius:var(--r-sm);cursor:pointer}
|
||
.cuenta-nav:hover{background:rgb(var(--panel))}
|
||
.avatar{width:26px;height:26px;border-radius:99px;background:rgb(var(--accent));color:#fff;display:grid;place-items:center;font-size:.68rem;font-weight:700;flex:none}
|
||
.avatar-lg{width:64px;height:64px;font-size:1.3rem}
|
||
html[data-navmode="compact"] .cuenta-nav .txt{display:none}
|
||
.cuenta-nav .txt{overflow:hidden}
|
||
.cuenta-nav .txt b{display:block;font-size:.8rem}
|
||
.cuenta-nav .txt small{color:rgb(var(--muted))}
|
||
|
||
/* col-derecha */
|
||
.col-derecha{flex:1;min-width:0;height:100%;display:flex;flex-direction:column;background:rgb(var(--canvas))}
|
||
|
||
/* barra-comandos (CommandBar) */
|
||
.barra-comandos{height:var(--h-comandos);flex:none;display:flex;align-items:center;gap:.35rem;padding:0 1rem;border-bottom:1px solid rgb(var(--line))}
|
||
.titulo-vista{font-family:var(--app-font-disp);font-weight:600;font-size:1.05rem;margin-right:.8rem;white-space:nowrap}
|
||
.cb-sep{width:1px;align-self:stretch;margin:.5rem .25rem;background:rgb(var(--line))}
|
||
.cb-derecha{margin-left:auto;display:flex;align-items:center;gap:.35rem}
|
||
|
||
/* migas-pan (BreadcrumbBar) */
|
||
.migas-pan{height:var(--h-migas);flex:none;display:flex;align-items:center;gap:.35rem;padding:0 1.1rem;font-size:.78rem;color:rgb(var(--muted));background:rgb(var(--panel)/.4);border-bottom:1px solid rgb(var(--line)/.7)}
|
||
.migas-pan b{color:rgb(var(--body));font-weight:600}
|
||
.migas-pan .sep{opacity:.6}
|
||
|
||
/* area-contenido: única con scroll propio */
|
||
.area-contenido{flex:1;min-height:0;overflow-y:auto;padding:1.2rem}
|
||
.area-contenido>section{display:none}
|
||
.area-contenido>section.on{display:block}
|
||
|
||
/* barra-estado */
|
||
.barra-estado{height:var(--h-estado);flex:none;display:flex;align-items:center;gap:1rem;padding:0 1rem;
|
||
background:rgb(var(--panel)/.5);border-top:1px solid rgb(var(--line));font-size:.72rem;color:rgb(var(--muted))}
|
||
.barra-estado .der{margin-left:auto;display:flex;align-items:center;gap:.8rem}
|
||
|
||
/* ====== rótulo de regiones/componentes (equivalente a "Nombres" del template web) ====== */
|
||
.region{position:relative}
|
||
.region-name,.comp-name{display:none;position:absolute;top:-9px;left:8px;font-size:.6rem;font-family:ui-monospace,monospace;
|
||
padding:1px 6px;border-radius:99px;z-index:60;pointer-events:none;white-space:nowrap}
|
||
.region-name{background:rgb(var(--accent));color:#fff}
|
||
.comp-name{background:rgb(var(--success));color:#04210f}
|
||
body.show-names .region-name,body.show-names .comp-name{display:inline-block}
|
||
body.show-names .region{outline:1px dashed rgb(var(--accent)/.5);outline-offset:2px}
|
||
body.show-names .comp-tag{outline:1px dashed rgb(var(--success)/.6);outline-offset:2px;position:relative}
|
||
|
||
/* ====== Tarjetas / secciones de contenido ====== */
|
||
.card{background:rgb(var(--surface));border:1px solid rgb(var(--line));border-radius:var(--r-lg);padding:1rem;margin-bottom:1rem}
|
||
.card>h2{display:flex;align-items:center;gap:.5rem}
|
||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}
|
||
.row-wrap{display:flex;flex-wrap:wrap;gap:.6rem;align-items:center}
|
||
@media (max-width:900px){.grid-2,.grid-3{grid-template-columns:1fr}}
|
||
|
||
/* ====== Botones Fluent: accent / standard / subtle / hyperlink ====== */
|
||
button{font-family:inherit}
|
||
.btn{display:inline-flex;align-items:center;gap:.5rem;height:32px;padding:0 .85rem;border-radius:var(--r-sm);
|
||
font-size:.8rem;font-weight:600;cursor:pointer;border:1px solid rgb(var(--line-strong));background:rgb(var(--field));
|
||
color:rgb(var(--body));transition:background .1s,border-color .1s}
|
||
.btn svg{width:14px;height:14px;flex:none}
|
||
.btn:hover{background:rgb(var(--panel))}
|
||
.btn:active{transform:translateY(1px)}
|
||
.btn.accent{background:rgb(var(--accent));border-color:rgb(var(--accent));color:#fff}
|
||
.btn.accent:hover{background:rgb(var(--accent-2))}
|
||
.btn.subtle{border-color:transparent;background:transparent}
|
||
.btn.subtle:hover{background:rgb(var(--panel))}
|
||
.btn.danger{border-color:rgb(var(--danger));color:rgb(var(--danger))}
|
||
.btn.danger:hover{background:rgb(var(--danger)/.12)}
|
||
.btn.sm{height:26px;padding:0 .6rem;font-size:.74rem}
|
||
.btn.icon-only{width:32px;padding:0;justify-content:center}
|
||
.btn[disabled]{opacity:.45;cursor:not-allowed}
|
||
.btn-link{background:none;border:0;color:rgb(var(--accent));font-size:.8rem;font-weight:600;cursor:pointer;padding:0}
|
||
.btn-link:hover{text-decoration:underline}
|
||
|
||
/* ====== Tooltip (mouse-first, aparece con leve retardo) ====== */
|
||
.tip{position:relative}
|
||
.tip::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 7px);left:50%;transform:translateX(-50%) translateY(3px);
|
||
background:rgb(var(--panel));color:rgb(var(--body));border:1px solid rgb(var(--line-strong));font-size:.7rem;
|
||
padding:.3rem .55rem;border-radius:var(--r-sm);white-space:nowrap;opacity:0;pointer-events:none;
|
||
transition:opacity .12s .35s,transform .12s .35s;box-shadow:var(--shadow-flyout);z-index:400}
|
||
.tip:hover::after{opacity:1;transform:translateX(-50%) translateY(0)}
|
||
|
||
/* ====== Campos: TextBox / NumberBox / ComboBox / Textarea ====== */
|
||
.field{display:flex;flex-direction:column;gap:.3rem}
|
||
.field label{font-size:.74rem;font-weight:600;color:rgb(var(--muted))}
|
||
.field .req::after{content:' *';color:rgb(var(--danger))}
|
||
.field .hint{font-size:.7rem;color:rgb(var(--muted))}
|
||
.field .msg-error{font-size:.7rem;color:rgb(var(--danger))}
|
||
.field .msg-ok{font-size:.7rem;color:rgb(var(--success))}
|
||
.txt,textarea.txt,.numbox input{width:100%;height:32px;border-radius:var(--r-sm);border:1px solid rgb(var(--line-strong));
|
||
background:rgb(var(--field));color:rgb(var(--body));font-size:.82rem;padding:0 .6rem;font-family:inherit}
|
||
textarea.txt{height:auto;padding:.5rem .6rem;resize:vertical;min-height:70px}
|
||
.txt:hover{border-color:rgb(var(--muted))}
|
||
.txt:focus{border-color:rgb(var(--accent));box-shadow:0 0 0 1px rgb(var(--accent))}
|
||
.field.error .txt{border-color:rgb(var(--danger))}
|
||
.field.ok .txt{border-color:rgb(var(--success))}
|
||
.txt:disabled,.txt[readonly]{opacity:.55;cursor:not-allowed}
|
||
.numbox{display:flex;align-items:stretch;border-radius:var(--r-sm);overflow:hidden;border:1px solid rgb(var(--line-strong))}
|
||
.numbox input{border:0;border-radius:0;text-align:center}
|
||
.numbox button{width:28px;border:0;background:rgb(var(--panel));color:rgb(var(--body));cursor:pointer;border-left:1px solid rgb(var(--line))}
|
||
.numbox button:hover{background:rgb(var(--line))}
|
||
|
||
/* ComboBox (desplegable propio, redondeado, con búsqueda si ≥10 opciones) */
|
||
.combo{position:relative}
|
||
.combo-trigger{width:100%;height:32px;border-radius:var(--r-sm);border:1px solid rgb(var(--line-strong));background:rgb(var(--field));
|
||
display:flex;align-items:center;justify-content:space-between;padding:0 .6rem;font-size:.82rem;cursor:pointer}
|
||
.combo-trigger:hover{border-color:rgb(var(--muted))}
|
||
.combo.open .combo-trigger{border-color:rgb(var(--accent))}
|
||
.combo-trigger svg{width:12px;height:12px;color:rgb(var(--muted));transition:transform .12s}
|
||
.combo.open .combo-trigger svg{transform:rotate(180deg)}
|
||
.combo-panel{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;background:rgb(var(--surface));
|
||
border:1px solid rgb(var(--line));border-radius:var(--r-lg);box-shadow:var(--shadow-flyout);z-index:300;padding:.35rem;max-height:230px;overflow:auto}
|
||
.combo.open .combo-panel{display:block}
|
||
.combo-search{width:100%;height:28px;border-radius:var(--r-sm);border:1px solid rgb(var(--line));background:rgb(var(--field));
|
||
padding:0 .5rem;font-size:.78rem;margin-bottom:.3rem;color:rgb(var(--body))}
|
||
.combo-opt{padding:.4rem .55rem;border-radius:var(--r-sm);font-size:.8rem;cursor:pointer}
|
||
.combo-opt:hover{background:rgb(var(--panel))}
|
||
.combo-opt.sel{background:rgb(var(--accent)/.16);color:rgb(var(--accent));font-weight:600}
|
||
.combo-opt.hidden{display:none}
|
||
|
||
/* CheckBox / RadioButtons */
|
||
.choice{display:inline-flex;align-items:center;gap:.5rem;font-size:.82rem;cursor:pointer}
|
||
.choice input{width:16px;height:16px;accent-color:rgb(var(--accent))}
|
||
|
||
/* ToggleSwitch */
|
||
.toggle{display:inline-flex;align-items:center;gap:.6rem;cursor:pointer;font-size:.82rem}
|
||
.toggle input{display:none}
|
||
.toggle .track{width:38px;height:20px;border-radius:99px;background:transparent;border:1px solid rgb(var(--muted));position:relative;transition:.15s;flex:none}
|
||
.toggle .track::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:rgb(var(--muted));transition:.15s}
|
||
.toggle input:checked + .track{background:rgb(var(--accent));border-color:rgb(var(--accent))}
|
||
.toggle input:checked + .track::after{transform:translateX(18px);background:#fff}
|
||
|
||
/* Slider */
|
||
.slider{-webkit-appearance:none;width:100%;height:4px;border-radius:99px;background:rgb(var(--line-strong));outline:none}
|
||
.slider::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:rgb(var(--accent));border:3px solid rgb(var(--surface));box-shadow:0 0 0 1px rgb(var(--accent));cursor:pointer;margin-top:-6px}
|
||
|
||
/* DatePicker (calendario propio en flyout) */
|
||
.dp{position:relative}
|
||
.dp-trigger{width:100%;height:32px;border-radius:var(--r-sm);border:1px solid rgb(var(--line-strong));background:rgb(var(--field));
|
||
display:flex;align-items:center;gap:.5rem;padding:0 .6rem;font-size:.82rem;cursor:pointer}
|
||
.dp-trigger:hover{border-color:rgb(var(--muted))}
|
||
.dp-trigger svg{width:14px;height:14px;color:rgb(var(--muted))}
|
||
.dp-panel{display:none;position:absolute;top:calc(100% + 4px);left:0;background:rgb(var(--surface));border:1px solid rgb(var(--line));
|
||
border-radius:var(--r-lg);box-shadow:var(--shadow-flyout);z-index:300;padding:.7rem;width:230px}
|
||
.dp.open .dp-panel{display:block}
|
||
.dp-head{display:flex;align-items:center;margin-bottom:.5rem}
|
||
.dp-head b{flex:1;text-align:center;font-size:.8rem}
|
||
.dp-head button{border:0;background:transparent;color:rgb(var(--body));cursor:pointer;width:24px;height:24px;border-radius:var(--r-sm)}
|
||
.dp-head button:hover{background:rgb(var(--panel))}
|
||
.dp-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;font-size:.72rem;text-align:center}
|
||
.dp-grid .dow{color:rgb(var(--muted));padding:.25rem 0;font-weight:600}
|
||
.dp-grid button{border:0;background:transparent;color:rgb(var(--body));padding:.35rem 0;border-radius:var(--r-sm);cursor:pointer}
|
||
.dp-grid button:hover{background:rgb(var(--panel))}
|
||
.dp-grid button.out{color:rgb(var(--muted)/.5)}
|
||
.dp-grid button.today{border:1px solid rgb(var(--accent))}
|
||
.dp-grid button.sel{background:rgb(var(--accent));color:#fff}
|
||
|
||
/* Rating */
|
||
.rating{display:flex;gap:.15rem}
|
||
.rating svg{width:18px;height:18px;color:rgb(var(--line-strong));cursor:pointer}
|
||
.rating svg.on{color:rgb(var(--warning))}
|
||
|
||
/* Dropzone (uso ligero: la mayoría de apps Windows usan el diálogo nativo "Examinar…") */
|
||
.dropzone{border:1.5px dashed rgb(var(--line-strong));border-radius:var(--r-lg);padding:1rem;text-align:center;color:rgb(var(--muted));font-size:.8rem;cursor:pointer}
|
||
.dropzone:hover{border-color:rgb(var(--accent));color:rgb(var(--body));background:rgb(var(--accent)/.06)}
|
||
|
||
/* Badge / StatusBadge */
|
||
.badge{display:inline-flex;align-items:center;gap:.3rem;padding:.15rem .55rem;border-radius:99px;font-size:.68rem;font-weight:700}
|
||
.badge.info{background:rgb(var(--accent)/.18);color:rgb(var(--accent))}
|
||
.badge.success{background:rgb(var(--success)/.18);color:rgb(var(--success))}
|
||
.badge.warning{background:rgb(var(--warning)/.18);color:rgb(var(--warning))}
|
||
.badge.danger{background:rgb(var(--danger)/.18);color:rgb(var(--danger))}
|
||
.dot{width:6px;height:6px;border-radius:50%;background:currentColor}
|
||
|
||
/* ====== KPI + Skeleton + Spinner + Progress ====== */
|
||
.kpi{border:1px solid rgb(var(--line));border-radius:var(--r-lg);padding:.9rem;background:rgb(var(--panel)/.4)}
|
||
.kpi .k-label{font-size:.72rem;color:rgb(var(--muted))}
|
||
.kpi .k-value{font-family:var(--app-font-disp);font-size:1.6rem;font-weight:600;margin-top:.15rem}
|
||
.kpi .k-delta{font-size:.72rem;font-weight:700} .kpi .up{color:rgb(var(--success))} .kpi .down{color:rgb(var(--danger))}
|
||
.spinner{width:20px;height:20px;border:2.5px solid rgb(var(--line));border-top-color:rgb(var(--accent));border-radius:50%;animation:spin .7s linear infinite;display:inline-block}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.skeleton{display:block;height:11px;border-radius:4px;background:linear-gradient(90deg,rgb(var(--panel)) 25%,rgb(var(--line)) 37%,rgb(var(--panel)) 63%);background-size:400% 100%;animation:shimmer 1.3s infinite}
|
||
@keyframes shimmer{0%{background-position:100% 0}100%{background-position:-100% 0}}
|
||
.progress{display:block;height:5px;border-radius:99px;background:rgb(var(--panel));overflow:hidden}
|
||
.progress>i{display:block;height:100%;background:rgb(var(--accent));border-radius:99px}
|
||
.chart-box{height:220px}
|
||
|
||
/* ====== ListView / DataGrid ====== */
|
||
.toolbar-lv{display:flex;flex-wrap:wrap;align-items:center;gap:.6rem;margin-bottom:.7rem}
|
||
.buscador{position:relative;max-width:230px;flex:1;min-width:160px}
|
||
.buscador svg{position:absolute;left:.55rem;top:50%;transform:translateY(-50%);width:14px;height:14px;color:rgb(var(--muted))}
|
||
.buscador input{padding-left:1.9rem}
|
||
.lv-wrap{border:1px solid rgb(var(--line));border-radius:var(--r-lg);overflow:hidden;background:rgb(var(--surface))}
|
||
table.lv{width:100%;border-collapse:collapse;font-size:.8rem}
|
||
table.lv thead th{position:sticky;top:0;background:rgb(var(--panel));text-align:left;font-size:.7rem;font-weight:700;color:rgb(var(--muted));
|
||
padding:.55rem .7rem;border-bottom:1px solid rgb(var(--line));cursor:pointer;white-space:nowrap;user-select:none}
|
||
table.lv thead th:hover{color:rgb(var(--body))}
|
||
table.lv thead th .sort-ico{width:10px;height:10px;margin-left:.25rem;opacity:.4;display:inline-block;vertical-align:middle}
|
||
table.lv thead th.sorted .sort-ico{opacity:1;color:rgb(var(--accent))}
|
||
table.lv td{padding:.55rem .7rem;border-bottom:1px solid rgb(var(--line)/.6)}
|
||
table.lv tbody tr{cursor:pointer}
|
||
table.lv tbody tr:hover{background:rgb(var(--panel)/.6)}
|
||
table.lv tbody tr.sel{background:rgb(var(--accent)/.14)}
|
||
table.lv tbody tr.filler{visibility:hidden}
|
||
.lv-chk{width:16px;height:16px;accent-color:rgb(var(--accent))}
|
||
.pie-listado{display:flex;flex-wrap:wrap;align-items:center;gap:.9rem;padding:.6rem .8rem;border-top:1px solid rgb(var(--line));font-size:.74rem;color:rgb(var(--muted))}
|
||
.paginacion{margin-left:auto;display:flex;align-items:center;gap:.2rem}
|
||
.paginacion button{width:26px;height:26px;border:1px solid rgb(var(--line));background:rgb(var(--field));color:rgb(var(--body));border-radius:var(--r-sm);cursor:pointer;font-size:.74rem}
|
||
.paginacion button:hover:not(:disabled){background:rgb(var(--panel))}
|
||
.paginacion button.act{background:rgb(var(--accent));border-color:rgb(var(--accent));color:#fff}
|
||
.paginacion button:disabled{opacity:.35;cursor:not-allowed}
|
||
|
||
/* ====== Maestro-detalle (split view) ====== */
|
||
.split{display:flex;gap:0;border:1px solid rgb(var(--line));border-radius:var(--r-lg);overflow:hidden;height:460px;background:rgb(var(--surface))}
|
||
.split-list{width:300px;flex:none;overflow-y:auto;border-right:1px solid rgb(var(--line))}
|
||
.split-item{padding:.7rem .9rem;border-bottom:1px solid rgb(var(--line)/.6);cursor:pointer}
|
||
.split-item:hover{background:rgb(var(--panel)/.6)}
|
||
.split-item.sel{background:rgb(var(--accent)/.14);border-left:3px solid rgb(var(--accent))}
|
||
.split-item b{display:block;font-size:.83rem}
|
||
.split-item span{font-size:.72rem;color:rgb(var(--muted))}
|
||
.divisor{width:5px;flex:none;cursor:col-resize;background:transparent;position:relative}
|
||
.divisor:hover,.divisor.dragging{background:rgb(var(--accent)/.4)}
|
||
.split-detalle{flex:1;min-width:0;overflow-y:auto;padding:1.1rem}
|
||
|
||
/* ====== Árbol (Treeview, estilo Explorador) ====== */
|
||
ul.tree,ul.tree ul{list-style:none;margin:0;padding-left:1.1rem;font-size:.82rem}
|
||
ul.tree{padding-left:0}
|
||
ul.tree li{position:relative}
|
||
.node{display:flex;align-items:center;gap:.35rem;padding:.32rem .4rem;border-radius:var(--r-sm);cursor:pointer}
|
||
.node:hover{background:rgb(var(--panel))}
|
||
.node.sel{background:rgb(var(--accent)/.16);color:rgb(var(--accent));font-weight:600}
|
||
.twig{width:14px;height:14px;flex:none;display:inline-flex;align-items:center;justify-content:center;color:rgb(var(--muted));transition:transform .12s}
|
||
li.collapsed>ul{display:none}
|
||
li.collapsed>.node .twig{transform:rotate(-90deg)}
|
||
.node .ico{width:15px;height:15px;flex:none;color:rgb(var(--accent))}
|
||
.leaf .ico{color:rgb(var(--muted))}
|
||
|
||
/* ====== Expander (patrón Configuración de Windows 11) ====== */
|
||
.expander{border:1px solid rgb(var(--line));border-radius:var(--r-lg);overflow:hidden;margin-bottom:.6rem}
|
||
.exp-head{display:flex;align-items:center;gap:.7rem;padding:.75rem .9rem;cursor:pointer;background:rgb(var(--surface))}
|
||
.exp-head:hover{background:rgb(var(--panel)/.5)}
|
||
.exp-head .ico{width:19px;height:19px;color:rgb(var(--accent));flex:none}
|
||
.exp-head .txt{flex:1;min-width:0}
|
||
.exp-head .txt b{display:block;font-size:.83rem}
|
||
.exp-head .txt span{font-size:.72rem;color:rgb(var(--muted))}
|
||
.exp-head .chev{width:13px;height:13px;color:rgb(var(--muted));transition:transform .12s;flex:none}
|
||
.expander.open .exp-head .chev{transform:rotate(180deg)}
|
||
.exp-body{display:none;padding:.9rem;border-top:1px solid rgb(var(--line));background:rgb(var(--panel)/.3)}
|
||
.expander.open .exp-body{display:block}
|
||
|
||
/* ====== Menú contextual (click derecho) y menús desplegables (flyout de menú) ====== */
|
||
.menu-flotante{position:fixed;min-width:210px;background:rgb(var(--surface));border:1px solid rgb(var(--line));
|
||
border-radius:var(--r-lg);box-shadow:var(--shadow-flyout);padding:.35rem;z-index:900;display:none;font-size:.82rem}
|
||
.menu-flotante.open{display:block}
|
||
.mi{display:flex;align-items:center;gap:.6rem;padding:.45rem .6rem;border-radius:var(--r-sm);cursor:pointer;position:relative}
|
||
.mi:hover{background:rgb(var(--accent));color:#fff}
|
||
.mi:hover .accel{color:#fff}
|
||
.mi svg{width:15px;height:15px;flex:none}
|
||
.mi .lbl{flex:1}
|
||
.mi .accel{font-size:.68rem;color:rgb(var(--muted))}
|
||
.mi .arrow{width:11px;height:11px;color:rgb(var(--muted))}
|
||
.mi:hover .arrow{color:#fff}
|
||
.mi.danger{color:rgb(var(--danger))}
|
||
.mi.danger:hover{background:rgb(var(--danger));color:#fff}
|
||
.mi-sep{height:1px;background:rgb(var(--line));margin:.3rem .2rem}
|
||
.submenu{position:absolute;left:100%;top:-.35rem;display:none}
|
||
.mi:hover>.submenu{display:block}
|
||
|
||
/* barra de menú clásica (File Edit View…) — opcional para apps de productividad */
|
||
.menu-bar{display:flex;gap:.15rem;height:30px;align-items:center;padding:0 .3rem;border-bottom:1px solid rgb(var(--line));background:rgb(var(--surface))}
|
||
.menu-bar>.mb-item{position:relative;height:100%;display:flex;align-items:center;padding:0 .6rem;font-size:.78rem;border-radius:var(--r-sm);cursor:pointer}
|
||
.menu-bar>.mb-item:hover,.menu-bar>.mb-item.open{background:rgb(var(--panel))}
|
||
.menu-bar .menu-flotante{position:absolute;top:100%;left:0;min-width:190px}
|
||
|
||
/* Flyout genérico (popover anclado, light-dismiss) */
|
||
.flyout{position:fixed;background:rgb(var(--surface));border:1px solid rgb(var(--line));border-radius:var(--r-lg);
|
||
box-shadow:var(--shadow-flyout);padding:.9rem;z-index:850;display:none;width:260px;font-size:.82rem}
|
||
.flyout.open{display:block}
|
||
.flyout h4{margin:0 0 .3rem;font-size:.85rem;font-weight:700}
|
||
.flyout .flyout-foot{display:flex;justify-content:flex-end;gap:.4rem;margin-top:.7rem}
|
||
|
||
/* ====== ContentDialog (modal) — REGLA DE ORO para decisiones/avisos ====== */
|
||
.overlay{position:fixed;inset:0;background:rgb(0 0 0/.45);display:none;align-items:center;justify-content:center;z-index:950;backdrop-filter:blur(2px)}
|
||
.overlay.open{display:flex}
|
||
.cdialog{width:min(420px,90vw);background:rgb(var(--surface));border-radius:var(--r-lg);box-shadow:var(--shadow-win);
|
||
border:1px solid rgb(var(--line));overflow:hidden}
|
||
.cdialog .cd-bar{height:4px}
|
||
.cdialog .cd-bar.info{background:rgb(var(--accent))} .cdialog .cd-bar.success{background:rgb(var(--success))}
|
||
.cdialog .cd-bar.warning{background:rgb(var(--warning))} .cdialog .cd-bar.error{background:rgb(var(--danger))}
|
||
.cdialog .cd-body{padding:1.2rem 1.3rem}
|
||
.cdialog .cd-title{display:flex;align-items:center;gap:.6rem;font-family:var(--app-font-disp);font-size:1.05rem;font-weight:600;margin-bottom:.45rem}
|
||
.cdialog .cd-title svg{width:19px;height:19px;flex:none}
|
||
.cdialog .cd-title.info svg{color:rgb(var(--accent))} .cdialog .cd-title.success svg{color:rgb(var(--success))}
|
||
.cdialog .cd-title.warning svg{color:rgb(var(--warning))} .cdialog .cd-title.error svg{color:rgb(var(--danger))}
|
||
.cdialog .cd-msg{font-size:.85rem;color:rgb(var(--muted));line-height:1.55}
|
||
.cdialog .cd-foot{display:flex;justify-content:flex-end;gap:.5rem;padding:.9rem 1.3rem;background:rgb(var(--panel)/.4);border-top:1px solid rgb(var(--line))}
|
||
.cdialog .cd-foot .btn{flex:1;max-width:140px;justify-content:center}
|
||
|
||
/* ====== InfoBar (banner en línea, no bloqueante) ====== */
|
||
.infobar{display:flex;align-items:flex-start;gap:.6rem;border-radius:var(--r-sm);padding:.65rem .8rem;font-size:.8rem;margin-bottom:.6rem;border:1px solid}
|
||
.infobar svg{width:16px;height:16px;flex:none;margin-top:.05rem}
|
||
.infobar .ib-txt{flex:1}
|
||
.infobar .ib-txt b{display:block;margin-bottom:.1rem}
|
||
.infobar button.ib-x{border:0;background:transparent;color:inherit;cursor:pointer;opacity:.7;flex:none}
|
||
.infobar button.ib-x:hover{opacity:1}
|
||
.infobar.info{background:rgb(var(--accent)/.1);border-color:rgb(var(--accent)/.4);color:rgb(var(--accent))}
|
||
.infobar.success{background:rgb(var(--success)/.1);border-color:rgb(var(--success)/.4);color:rgb(var(--success))}
|
||
.infobar.warning{background:rgb(var(--warning)/.1);border-color:rgb(var(--warning)/.4);color:rgb(var(--warning))}
|
||
.infobar.error{background:rgb(var(--danger)/.1);border-color:rgb(var(--danger)/.4);color:rgb(var(--danger))}
|
||
.infobar .ib-txt,.infobar .ib-txt b{color:rgb(var(--body))}
|
||
|
||
/* ====== Toast (notificación, solo eventos de fondo — NUNCA para decisiones) ====== */
|
||
.toast-stack{position:fixed;right:16px;bottom:16px;display:flex;flex-direction:column-reverse;gap:.5rem;z-index:980}
|
||
.toast{width:300px;background:rgb(var(--surface));border:1px solid rgb(var(--line));border-radius:var(--r-lg);
|
||
box-shadow:var(--shadow-flyout);padding:.75rem .85rem;display:flex;gap:.6rem;align-items:flex-start;
|
||
animation:toast-in .18s ease}
|
||
@keyframes toast-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||
.toast svg{width:17px;height:17px;color:rgb(var(--accent));flex:none;margin-top:.1rem}
|
||
.toast b{display:block;font-size:.8rem} .toast span{font-size:.72rem;color:rgb(var(--muted))}
|
||
|
||
/* ====== Teaching Tip (coach mark) ====== */
|
||
.teaching-tip{position:fixed;width:250px;background:rgb(var(--surface));border:1px solid rgb(var(--accent));border-radius:var(--r-lg);
|
||
box-shadow:var(--shadow-flyout);padding:.9rem;z-index:960;display:none;font-size:.8rem}
|
||
.teaching-tip.open{display:block}
|
||
.teaching-tip::before{content:'';position:absolute;top:-7px;left:24px;width:12px;height:12px;background:rgb(var(--surface));
|
||
border-left:1px solid rgb(var(--accent));border-top:1px solid rgb(var(--accent));transform:rotate(45deg)}
|
||
.teaching-tip h4{margin:0 0 .3rem;font-size:.85rem;display:flex;align-items:center;gap:.4rem}
|
||
.teaching-tip h4 svg{width:15px;height:15px;color:rgb(var(--accent))}
|
||
|
||
/* ====== Ventanas de plantilla (login / propiedades / about) ====== */
|
||
.mini-win{width:340px;border-radius:var(--r-lg);border:1px solid rgb(var(--line));box-shadow:var(--shadow-flyout);overflow:hidden;background:rgb(var(--surface))}
|
||
.mini-win .barra-titulo{height:32px}
|
||
.rep-doc{border:1px solid rgb(var(--line));border-radius:var(--r-lg);padding:1rem;background:rgb(var(--surface))}
|
||
.rep-head{display:flex;justify-content:space-between;gap:1rem;padding-bottom:.7rem;border-bottom:1px solid rgb(var(--line));margin-bottom:.7rem}
|
||
.rep-table{width:100%;border-collapse:collapse;font-size:.82rem}
|
||
.rep-table th{text-align:left;font-size:.7rem;color:rgb(var(--muted));border-bottom:1px solid rgb(var(--line));padding:.4rem .5rem}
|
||
.rep-table td{padding:.4rem .5rem;border-bottom:1px solid rgb(var(--line)/.5)}
|
||
|
||
/* ====== Íconos (Lucide, local — sin CDN) ====== */
|
||
[data-lucide]{display:inline-block}
|
||
svg{stroke:currentColor}
|
||
.icon-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:.5rem;margin-top:.6rem}
|
||
.icon-tile{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.4rem;padding:.6rem .3rem;height:78px;
|
||
border:1px solid rgb(var(--line));border-radius:var(--r-sm);background:rgb(var(--panel)/.4);cursor:pointer}
|
||
.icon-tile:hover{border-color:rgb(var(--accent))}
|
||
.icon-tile svg{width:20px;height:20px}
|
||
.icon-tile .ico-name{font-size:.63rem;color:rgb(var(--muted));font-family:ui-monospace,monospace;text-align:center;line-height:1.15;
|
||
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.icon-tile.hidden{display:none}
|
||
|
||
code{font-family:ui-monospace,monospace;background:rgb(var(--panel));padding:.05rem .35rem;border-radius:3px;font-size:.85em}
|
||
table.spec{width:100%;border-collapse:collapse;font-size:.78rem}
|
||
table.spec th{text-align:left;color:rgb(var(--muted));font-size:.7rem;padding:.35rem .5rem;border-bottom:1px solid rgb(var(--line))}
|
||
table.spec td{padding:.4rem .5rem;border-bottom:1px solid rgb(var(--line)/.6)}
|
||
|
||
@media (max-width:1000px){.ventana{width:100vw;height:100vh;border-radius:0}.desktop{padding:0}.grid-2,.grid-3{grid-template-columns:1fr}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="desktop">
|
||
<div class="ventana" id="ventana">
|
||
|
||
<!-- ======== BARRA-TITULO (custom title bar, arrastrable) ======== -->
|
||
<div class="barra-titulo region" data-region>
|
||
<span class="region-name">barra-titulo</span>
|
||
<div class="marca-app"><span class="ico" data-lucide="layout-panel-left"></span> LinkedCorp Desktop</div>
|
||
<div class="acciones-titulo">
|
||
<button class="btn-win tip" data-tip="Minimizar" data-lucide-wrap><svg viewBox="0 0 10 10"><path d="M0 5h10" stroke="currentColor" stroke-width="1.2"/></svg></button>
|
||
<button class="btn-win maximizar tip" data-tip="Maximizar">
|
||
<svg viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" fill="none" stroke="currentColor" stroke-width="1.1"/></svg>
|
||
<div class="snap-preview">
|
||
<div class="snap-opt s2"><i></i><i></i></div>
|
||
<div class="snap-opt s3"><i></i><i></i></div>
|
||
<div class="snap-opt s4"><i></i><i></i><i></i><i></i></div>
|
||
</div>
|
||
</button>
|
||
<button class="btn-win cerrar tip" data-tip="Cerrar"><svg viewBox="0 0 10 10"><path d="M0 0l10 10M10 0L0 10" stroke="currentColor" stroke-width="1.2"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="app-shell">
|
||
<!-- ======== NAVEGACION (NavigationView, FIJA a la izquierda) ======== -->
|
||
<nav class="navegacion region" data-region>
|
||
<span class="region-name">navegacion</span>
|
||
<div class="nav-top">
|
||
<button class="nav-toggle" onclick="toggleNav()" title="Expandir/compactar"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
|
||
<div class="nav-buscar"><input class="nav-search" id="navSearch" placeholder="Buscar…" oninput="filterNav()"></div>
|
||
</div>
|
||
<a class="item-nav active comp-tag" data-view="resumen" onclick="showView('resumen',this)"><span class="comp-name">item-nav</span><span class="ico" data-lucide="layout-grid"></span><span class="lbl">Resumen</span></a>
|
||
<a class="item-nav" data-view="dashboard" onclick="showView('dashboard',this)"><span class="ico" data-lucide="area-chart"></span><span class="lbl">Dashboard</span></a>
|
||
<a class="item-nav" data-view="listado" onclick="showView('listado',this)"><span class="ico" data-lucide="table"></span><span class="lbl">Listado</span></a>
|
||
<a class="item-nav" data-view="maestrodetalle" onclick="showView('maestrodetalle',this)"><span class="ico" data-lucide="columns-2"></span><span class="lbl">Maestro-detalle</span></a>
|
||
<a class="item-nav" data-view="formulario" onclick="showView('formulario',this)"><span class="ico" data-lucide="square-pen"></span><span class="lbl">Formulario</span></a>
|
||
<a class="item-nav" data-view="dialogos" onclick="showView('dialogos',this)"><span class="ico" data-lucide="message-square-warning"></span><span class="lbl">Diálogos y avisos</span></a>
|
||
<a class="item-nav" data-view="menus" onclick="showView('menus',this)"><span class="ico" data-lucide="menu"></span><span class="lbl">Menús y flyouts</span></a>
|
||
<a class="item-nav" data-view="arbol" onclick="showView('arbol',this)"><span class="ico" data-lucide="folder-tree"></span><span class="lbl">Árbol / carpetas</span></a>
|
||
<a class="item-nav" data-view="plantillas" onclick="showView('plantillas',this)"><span class="ico" data-lucide="app-window"></span><span class="lbl">Plantillas</span></a>
|
||
<a class="item-nav" data-view="iconos" onclick="showView('iconos',this)"><span class="ico" data-lucide="shapes"></span><span class="lbl">Íconos</span></a>
|
||
<div class="nav-sep"></div>
|
||
<a class="item-nav" data-view="config" onclick="showView('config',this)"><span class="ico" data-lucide="settings"></span><span class="lbl">Configuración</span></a>
|
||
<div class="pie-navegacion">
|
||
<div class="cuenta-nav" id="cuentaNav" onclick="toggleFlyout(event,'cuentaNav','flyCuenta')">
|
||
<span class="avatar">MG</span>
|
||
<span class="txt"><b>Milton González</b><small>Administrador</small></span>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- ======== COLUMNA DERECHA ======== -->
|
||
<div class="col-derecha region" data-region>
|
||
<span class="region-name" style="left:auto;right:8px">col-derecha</span>
|
||
|
||
<!-- barra-comandos (CommandBar) -->
|
||
<div class="barra-comandos region" data-region>
|
||
<span class="region-name">barra-comandos</span>
|
||
<span class="titulo-vista" id="tituloVista">Resumen</span>
|
||
<div class="cb-sep"></div>
|
||
<button class="btn accent sm comp-tag" onclick="dlgNuevo()"><span class="comp-name">boton</span><svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>Nuevo</button>
|
||
<button class="btn sm" onclick="toastInfo('Actualizado','Se recargó la información visible.')"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-3-6.7M21 3v6h-6"/></svg>Actualizar</button>
|
||
<button class="btn sm" onclick="dlgEliminar()"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="round"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>Eliminar</button>
|
||
<div class="cb-derecha">
|
||
<button class="btn sm icon-only tip" data-tip="Más opciones" onclick="toggleMenu(event,'cbOverflow',this)"><svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- migas-pan (BreadcrumbBar) -->
|
||
<div class="migas-pan region" data-region>
|
||
<span class="region-name">migas-pan</span>
|
||
<span data-lucide="home" style="width:13px;height:13px"></span>
|
||
<span class="sep">›</span><b id="migaActual">Resumen</b>
|
||
</div>
|
||
|
||
<!-- area-contenido (única con scroll propio) -->
|
||
<div class="area-contenido region" data-region id="areaContenido">
|
||
<span class="region-name" style="left:auto;right:12px">area-contenido</span>
|
||
|
||
<!-- ===================== VISTA: RESUMEN ===================== -->
|
||
<section data-view="resumen" class="on">
|
||
<div class="card">
|
||
<h2><span data-lucide="info" style="width:18px;height:18px;color:rgb(var(--accent))"></span>Guía viva — identidad Windows</h2>
|
||
<p class="muted sm">Esta ventana simula el shell de una app de escritorio moderna (estilo Fluent / Windows 11), pensada para <b>mouse</b>: hover en cada control, <b>clic derecho</b> en las filas del listado, y arrastra el divisor en “Maestro-detalle”. Activa “Nombres” (barra de comandos › ⋯) para ver el nombre de cada región/componente y pedir cambios por su nombre exacto.</p>
|
||
<div class="row-wrap">
|
||
<label class="toggle"><input type="checkbox" id="chkNames" onchange="toggleNames()"><span class="track"></span>Mostrar nombres de región/componente</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Botones (Accent / Standard / Subtle / Danger / Hyperlink)</h2>
|
||
<div class="row-wrap">
|
||
<button class="btn accent">Accent</button>
|
||
<button class="btn">Standard</button>
|
||
<button class="btn subtle">Subtle</button>
|
||
<button class="btn danger">Danger</button>
|
||
<button class="btn sm accent">Sm accent</button>
|
||
<button class="btn" disabled>Deshabilitado</button>
|
||
<button class="btn-link" onclick="dlg('info')">Hyperlink</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<div class="card">
|
||
<h2>Colores (tokens)</h2>
|
||
<div class="row-wrap">
|
||
<span class="badge info"><i class="dot"></i>Info</span>
|
||
<span class="badge success"><i class="dot"></i>Éxito</span>
|
||
<span class="badge warning"><i class="dot"></i>Atención</span>
|
||
<span class="badge danger"><i class="dot"></i>Error</span>
|
||
</div>
|
||
<p class="muted xs" style="margin-top:.6rem">Acento del sistema (Windows deja elegir uno):</p>
|
||
<div class="row-wrap" id="accentSwatches"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Campos</h2>
|
||
<div class="field"><label class="req">Nombre</label><input class="txt" placeholder="Escribe aquí…"></div>
|
||
<div class="field" style="margin-top:.6rem"><label>Con error</label><input class="txt" value="dato inválido">
|
||
<span class="msg-error">Este campo es obligatorio.</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Estados de carga</h2>
|
||
<div class="row-wrap" style="align-items:flex-start">
|
||
<div><span class="spinner"></span></div>
|
||
<div style="width:160px;display:grid;gap:.35rem"><span class="skeleton" style="width:90%"></span><span class="skeleton" style="width:65%"></span></div>
|
||
<div style="width:160px"><span class="progress"><i style="width:64%"></i></span></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: DASHBOARD ===================== -->
|
||
<section data-view="dashboard">
|
||
<div class="card">
|
||
<h2>Indicadores</h2>
|
||
<div class="grid-3">
|
||
<div class="kpi"><div class="k-label">Ventas del mes</div><div class="k-value">$48,210</div><div class="k-delta up">▲ 12.4%</div></div>
|
||
<div class="kpi"><div class="k-label">Clientes activos</div><div class="k-value">1,284</div><div class="k-delta up">▲ 3.1%</div></div>
|
||
<div class="kpi"><div class="k-label">Tickets abiertos</div><div class="k-value">37</div><div class="k-delta down">▼ 5.0%</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="grid-2">
|
||
<div class="card"><h2>Ventas (línea)</h2><div class="chart-box"><canvas id="chLine"></canvas></div></div>
|
||
<div class="card"><h2>Distribución (dona)</h2><div class="chart-box"><canvas id="chDon"></canvas></div></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: LISTADO ===================== -->
|
||
<section data-view="listado">
|
||
<div class="card">
|
||
<h2>ListView — selección, orden, clic derecho</h2>
|
||
<p class="muted xs">Clic en una fila selecciona; <b>clic derecho</b> abre el menú contextual; clic en el encabezado ordena la columna.</p>
|
||
<div class="toolbar-lv">
|
||
<div class="buscador comp-tag"><span class="comp-name">buscador</span><svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg><input class="txt" id="lvSearch" placeholder="Buscar cliente…" oninput="renderLV()"></div>
|
||
<div class="combo" style="width:150px" id="cbEstado">
|
||
<div class="combo-trigger" onclick="toggleCombo(event,'cbEstado')"><span id="cbEstadoLbl">Todos</span><svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><path d="M6 9l6 6 6-6"/></svg></div>
|
||
<div class="combo-panel">
|
||
<div class="combo-opt sel" onclick="setCombo('cbEstado',this,'Todos')">Todos</div>
|
||
<div class="combo-opt" onclick="setCombo('cbEstado',this,'Activo')">Activo</div>
|
||
<div class="combo-opt" onclick="setCombo('cbEstado',this,'Pendiente')">Pendiente</div>
|
||
<div class="combo-opt" onclick="setCombo('cbEstado',this,'Inactivo')">Inactivo</div>
|
||
</div>
|
||
</div>
|
||
<span id="lvSelInfo" class="muted xs" style="margin-left:auto"></span>
|
||
</div>
|
||
<div class="lv-wrap comp-tag"><span class="comp-name" style="left:auto;right:8px">listview</span>
|
||
<table class="lv">
|
||
<thead><tr>
|
||
<th style="width:34px"><input type="checkbox" class="lv-chk" onclick="lvToggleAll(this)"></th>
|
||
<th data-k="cod" onclick="lvSort('cod')">Código<svg class="sort-ico" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></th>
|
||
<th data-k="nombre" onclick="lvSort('nombre')">Cliente<svg class="sort-ico" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></th>
|
||
<th data-k="monto" onclick="lvSort('monto')">Monto<svg class="sort-ico" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></th>
|
||
<th>Estado</th><th style="width:34px"></th>
|
||
</tr></thead>
|
||
<tbody id="lvBody"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pie-listado">
|
||
<span>Líneas por página:
|
||
<select id="lvPerPage" onchange="lvPage=1;renderLV()" style="background:rgb(var(--field));color:rgb(var(--body));border:1px solid rgb(var(--line));border-radius:4px;padding:.15rem .3rem">
|
||
<option>5</option><option selected>10</option><option>20</option><option>50</option><option>Todo</option>
|
||
</select>
|
||
</span>
|
||
<span id="lvRango"></span>
|
||
<div class="paginacion" id="lvPag"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: MAESTRO-DETALLE ===================== -->
|
||
<section data-view="maestrodetalle">
|
||
<div class="card">
|
||
<h2>Maestro-detalle (Split View)</h2>
|
||
<p class="muted xs">Patrón nativo de Windows (Correo, Configuración, Explorador): al seleccionar un ítem de la lista, el panel derecho se actualiza <b>en el sitio</b>, sin modal. Arrastra el divisor central para redimensionar.</p>
|
||
<div class="split comp-tag"><span class="comp-name">panel-detalle</span>
|
||
<div class="split-list" id="mdList"></div>
|
||
<div class="divisor" id="mdDiv"></div>
|
||
<div class="split-detalle" id="mdDetalle"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: FORMULARIO ===================== -->
|
||
<section data-view="formulario">
|
||
<div class="card">
|
||
<h2>Controles de formulario</h2>
|
||
<div class="grid-2">
|
||
<div class="field"><label class="req">TextBox</label><input class="txt" placeholder="Texto…"></div>
|
||
<div class="field"><label>NumberBox</label>
|
||
<div class="numbox"><button onclick="numStep(this,-1)">−</button><input class="txt" value="1" id="numQty"><button onclick="numStep(this,1)">+</button></div>
|
||
</div>
|
||
<div class="field"><label>ComboBox (búsqueda si ≥10)</label>
|
||
<div class="combo" id="cbCiudad">
|
||
<div class="combo-trigger" onclick="toggleCombo(event,'cbCiudad')"><span id="cbCiudadLbl">Selecciona…</span><svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><path d="M6 9l6 6 6-6"/></svg></div>
|
||
<div class="combo-panel"><input class="combo-search" placeholder="Buscar…" oninput="comboFilter(this)">
|
||
<div id="cbCiudadOpts"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="field"><label>DatePicker</label>
|
||
<div class="dp" id="dpEntrega">
|
||
<div class="dp-trigger" onclick="toggleDP(event,'dpEntrega')"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg><span id="dpEntregaLbl">Elegir fecha…</span></div>
|
||
<div class="dp-panel"><div class="dp-head"><button onclick="dpNav('dpEntrega',-1)">‹</button><b id="dpEntregaMes"></b><button onclick="dpNav('dpEntrega',1)">›</button></div><div class="dp-grid" id="dpEntregaGrid"></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="grid-2" style="margin-top:1rem">
|
||
<div class="field"><label>Textarea</label><textarea class="txt" rows="3" placeholder="Notas…"></textarea></div>
|
||
<div>
|
||
<div class="field"><label>Opciones</label>
|
||
<div class="row-wrap">
|
||
<label class="choice"><input type="checkbox" checked>Notificarme</label>
|
||
<label class="choice"><input type="radio" name="pref" checked>Correo</label>
|
||
<label class="choice"><input type="radio" name="pref">SMS</label>
|
||
</div>
|
||
</div>
|
||
<div class="field" style="margin-top:.6rem"><label>ToggleSwitch</label>
|
||
<label class="toggle"><input type="checkbox" checked><span class="track"></span>Activo</label>
|
||
</div>
|
||
<div class="field" style="margin-top:.6rem"><label>Slider</label><input type="range" class="slider" value="60"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: DIALOGOS ===================== -->
|
||
<section data-view="dialogos">
|
||
<div class="card">
|
||
<h2>ContentDialog — REGLA DE ORO para decisiones</h2>
|
||
<p class="muted xs">Nunca <code>alert()</code>/<code>confirm()</code>/MessageBox nativo. Toda decisión pasa por un ContentDialog propio, centrado, con acción primaria a la derecha.</p>
|
||
<div class="row-wrap">
|
||
<button class="btn" onclick="dlg('info')">Información</button>
|
||
<button class="btn" onclick="dlg('success')">Éxito</button>
|
||
<button class="btn" onclick="dlg('warning')">Atención</button>
|
||
<button class="btn" onclick="dlg('error')">Error</button>
|
||
<button class="btn accent" onclick="dlgAceptar()">Aceptar/Cancelar</button>
|
||
<button class="btn danger" onclick="dlgEliminar()">Eliminar (destructiva)</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>InfoBar — aviso no bloqueante embebido en el contenido</h2>
|
||
<div id="infobarHost">
|
||
<div class="infobar info"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 12h1v5h1"/></svg><div class="ib-txt"><b>Sincronización activa</b>Los cambios se guardan automáticamente cada 30s.</div><button class="ib-x" onclick="this.closest('.infobar').remove()">✕</button></div>
|
||
<div class="infobar warning"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M12 3l10 18H2L12 3z"/><path d="M12 10v4M12 17h.01"/></svg><div class="ib-txt"><b>Licencia por vencer</b>Quedan 5 días de tu plan actual.</div><button class="ib-x" onclick="this.closest('.infobar').remove()">✕</button></div>
|
||
</div>
|
||
<button class="btn sm" onclick="addInfoBar()">+ Agregar InfoBar</button>
|
||
</div>
|
||
<div class="grid-2">
|
||
<div class="card">
|
||
<h2>Toast — solo eventos de fondo</h2>
|
||
<p class="muted xs">Para procesos sin interacción (ej. "Copia de seguridad completada"). No sirve para pedir una decisión.</p>
|
||
<button class="btn sm" onclick="toastInfo('Copia de seguridad completa','Se respaldaron 214 archivos.')">Simular notificación</button>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Teaching Tip — guía contextual</h2>
|
||
<p class="muted xs">Coach mark anclado a un control, para onboarding de una función nueva.</p>
|
||
<button class="btn sm" id="ttAnchor" onclick="showTeachingTip()">Mostrar consejo</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: MENUS ===================== -->
|
||
<section data-view="menus">
|
||
<div class="card">
|
||
<h2>Barra de menú clásica (opcional, apps de productividad)</h2>
|
||
<div class="menu-bar" id="menuBar">
|
||
<div class="mb-item" onclick="toggleMbMenu(event,this,'mArchivo')">Archivo
|
||
<div class="menu-flotante" id="mArchivo">
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M12 5v14M5 12h14"/></svg><span class="lbl">Nuevo</span><span class="accel">Ctrl+N</span></div>
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/></svg><span class="lbl">Guardar</span><span class="accel">Ctrl+S</span></div>
|
||
<div class="mi-sep"></div>
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M6 9V2h12v7M6 18h12v4H6zM6 14h12v-1a3 3 0 0 0-3-3H9a3 3 0 0 0-3 3z"/></svg><span class="lbl">Imprimir</span><span class="accel">Ctrl+P</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="mb-item" onclick="toggleMbMenu(event,this,'mEditar')">Editar
|
||
<div class="menu-flotante" id="mEditar">
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M9 14l2 2 4-4"/></svg><span class="lbl">Deshacer</span><span class="accel">Ctrl+Z</span></div>
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M9 14l2 2 4-4"/></svg><span class="lbl">Rehacer</span><span class="accel">Ctrl+Y</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="mb-item" onclick="toggleMbMenu(event,this,'mVer')">Ver
|
||
<div class="menu-flotante" id="mVer">
|
||
<div class="mi"><span class="lbl">Íconos grandes</span></div>
|
||
<div class="mi"><span class="lbl">Lista</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="muted xs" style="margin-top:.6rem">Se abre con clic; también responde a <b>clic derecho</b> en cualquier punto de esta tarjeta.</p>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<div class="card">
|
||
<h2>Menú contextual (clic derecho)</h2>
|
||
<div class="dropzone comp-tag" oncontextmenu="return openCtxDemo(event)" onclick="openCtxDemo(event)"><span class="comp-name">menucontextual</span>Clic derecho (o clic) aquí para ver el menú</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Flyout de usuario</h2>
|
||
<p class="muted xs">Anclado al avatar del panel de navegación (ábrelo desde la esquina inferior izquierda).</p>
|
||
<button class="btn sm" onclick="toggleFlyout(event,'cuentaNav','flyCuenta')">Abrir flyout de cuenta</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: ARBOL ===================== -->
|
||
<section data-view="arbol">
|
||
<div class="card">
|
||
<h2>Árbol jerárquico (estilo Explorador)</h2>
|
||
<ul class="tree" id="treeRoot"></ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: CONFIG ===================== -->
|
||
<section data-view="config">
|
||
<div class="card">
|
||
<h2>Configuración (patrón Expander de Windows 11)</h2>
|
||
<div class="expander open">
|
||
<div class="exp-head" onclick="this.closest('.expander').classList.toggle('open')">
|
||
<span class="ico" data-lucide="palette"></span>
|
||
<div class="txt"><b>Personalización</b><span>Tema, acento e idioma</span></div>
|
||
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||
</div>
|
||
<div class="exp-body">
|
||
<div class="row-wrap" style="margin-bottom:.8rem">
|
||
<span class="sm">Tema</span>
|
||
<button class="btn sm" onclick="setTheme('dark')">Oscuro</button>
|
||
<button class="btn sm" onclick="setTheme('light')">Claro</button>
|
||
</div>
|
||
<div class="row-wrap" style="margin-bottom:.8rem">
|
||
<span class="sm">Idioma</span>
|
||
<button class="btn sm" onclick="setLang('es')">Español</button>
|
||
<button class="btn sm" onclick="setLang('en')">English</button>
|
||
</div>
|
||
<div class="row-wrap"><span class="sm">Acento del sistema</span><div id="accentSwatches2"></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="expander">
|
||
<div class="exp-head" onclick="this.closest('.expander').classList.toggle('open')">
|
||
<span class="ico" data-lucide="bell"></span>
|
||
<div class="txt"><b>Notificaciones</b><span>Controla los avisos de la aplicación</span></div>
|
||
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||
</div>
|
||
<div class="exp-body">
|
||
<label class="toggle" style="margin-bottom:.6rem"><input type="checkbox" checked><span class="track"></span>Notificaciones en pantalla</label><br>
|
||
<label class="toggle"><input type="checkbox"><span class="track"></span>Sonido</label>
|
||
</div>
|
||
</div>
|
||
<div class="expander">
|
||
<div class="exp-head" onclick="this.closest('.expander').classList.toggle('open')">
|
||
<span class="ico" data-lucide="shield"></span>
|
||
<div class="txt"><b>Cuenta y seguridad</b><span>Sesión, cierre y datos</span></div>
|
||
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
||
</div>
|
||
<div class="exp-body"><button class="btn danger sm" onclick="dlgLogout()">Cerrar sesión</button></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: PLANTILLAS ===================== -->
|
||
<section data-view="plantillas">
|
||
<div class="grid-2">
|
||
<div class="card">
|
||
<h2>Ventana de inicio de sesión</h2>
|
||
<div style="display:flex;justify-content:center;padding:1rem 0">
|
||
<div class="mini-win">
|
||
<div class="barra-titulo"><div class="marca-app" style="padding-left:.6rem">Iniciar sesión</div><div class="acciones-titulo"><button class="btn-win cerrar" style="width:32px"><svg viewBox="0 0 10 10"><path d="M0 0l10 10M10 0L0 10" stroke="currentColor" stroke-width="1.2"/></svg></button></div></div>
|
||
<div style="padding:1.3rem">
|
||
<div style="display:flex;justify-content:center;margin-bottom:.9rem"><span class="avatar avatar-lg">MG</span></div>
|
||
<div class="field" style="margin-bottom:.6rem"><label>Usuario</label><input class="txt" placeholder="usuario@linkedcorp.net"></div>
|
||
<div class="field" style="margin-bottom:.6rem"><label>Contraseña</label><input class="txt" type="password" value="••••••••"></div>
|
||
<label class="choice" style="margin-bottom:.9rem"><input type="checkbox">Recordarme</label>
|
||
<button class="btn accent" style="width:100%;justify-content:center">Iniciar sesión</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Ventana “Acerca de”</h2>
|
||
<div style="display:flex;justify-content:center;padding:1rem 0">
|
||
<div class="mini-win">
|
||
<div class="barra-titulo"><div class="marca-app" style="padding-left:.6rem">Acerca de</div><div class="acciones-titulo"><button class="btn-win cerrar" style="width:32px"><svg viewBox="0 0 10 10"><path d="M0 0l10 10M10 0L0 10" stroke="currentColor" stroke-width="1.2"/></svg></button></div></div>
|
||
<div style="padding:1.3rem;text-align:center">
|
||
<span class="ico" data-lucide="layout-panel-left" style="width:34px;height:34px;color:rgb(var(--accent))"></span>
|
||
<h3 style="margin:.6rem 0 .1rem">LinkedCorp Desktop</h3>
|
||
<p class="muted xs">Versión 1.4.0 — © 2026 LinkedCorp</p>
|
||
<button class="btn sm" style="margin-top:.5rem">Buscar actualizaciones</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Reporte / vista previa de impresión</h2>
|
||
<div class="rep-doc print-area">
|
||
<div class="rep-head">
|
||
<div><b>LinkedCorp S.A.</b><br><span class="muted xs">RUC 1790000000001</span></div>
|
||
<div style="text-align:right"><b>Factura N° 001-001-0000123</b><br><span class="muted xs">01/09/2026</span></div>
|
||
</div>
|
||
<table class="rep-table">
|
||
<thead><tr><th>Descripción</th><th>Cant.</th><th>Precio</th><th>Total</th></tr></thead>
|
||
<tbody><tr><td>Servicio de soporte</td><td>1</td><td>$120.00</td><td>$120.00</td></tr></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="row-wrap" style="margin-top:.7rem"><button class="btn sm" onclick="window.print()">Imprimir</button><button class="btn sm">Exportar PDF</button></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== VISTA: ICONOS ===================== -->
|
||
<section data-view="iconos">
|
||
<div class="card">
|
||
<h2>Galería de íconos (Lucide, local)</h2>
|
||
<p class="muted xs">Un solo set en toda la app; los íconos heredan el color del contexto (activo = acento). Alternativa 100% nativa: <b>Segoe Fluent Icons</b> (fuente del sistema Windows) manteniendo la misma nomenclatura de claves.</p>
|
||
<div class="buscador" style="max-width:260px;margin-bottom:.3rem"><svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg><input class="txt" id="icoSearch" placeholder="Buscar ícono…" oninput="icoPage=1;renderIcons()"></div>
|
||
<div class="icon-gallery" id="icoGallery"></div>
|
||
<div class="pie-listado"><span id="icoRango"></span><div class="paginacion" id="icoPag"></div></div>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<!-- barra-estado (status bar) -->
|
||
<div class="barra-estado region" data-region>
|
||
<span class="region-name" style="top:-18px">barra-estado</span>
|
||
<span id="statusLeft">Listo</span>
|
||
<div class="der">
|
||
<span id="statusSel"></span>
|
||
<span data-lucide="wifi" style="width:13px;height:13px"></span>
|
||
<span>v1.4.0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ======== overlays globales ======== -->
|
||
<div class="overlay" id="dlgOverlay">
|
||
<div class="cdialog">
|
||
<div class="cd-bar" id="cdBar"></div>
|
||
<div class="cd-body">
|
||
<div class="cd-title" id="cdTitle"></div>
|
||
<div class="cd-msg" id="cdMsg"></div>
|
||
</div>
|
||
<div class="cd-foot" id="cdFoot"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="menu-flotante" id="ctxMenu"></div>
|
||
<div class="menu-flotante" id="cbOverflow">
|
||
<div class="mi" onclick="toggleNames()"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"/><circle cx="12" cy="12" r="3"/></svg><span class="lbl">Mostrar/ocultar nombres</span></div>
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M6 9V2h12v7M6 18h12v4H6zM6 14h12v-1a3 3 0 0 0-3-3H9a3 3 0 0 0-3 3z"/></svg><span class="lbl">Imprimir</span><span class="accel">Ctrl+P</span></div>
|
||
<div class="mi"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M12 3v12M7 10l5 5 5-5M5 21h14"/></svg><span class="lbl">Exportar</span><span class="arrow">▸</span>
|
||
<div class="submenu menu-flotante open" style="display:none">
|
||
<div class="mi"><span class="lbl">CSV</span></div><div class="mi"><span class="lbl">Excel</span></div><div class="mi"><span class="lbl">PDF</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flyout" id="flyCuenta">
|
||
<h4>Milton González</h4>
|
||
<p class="muted xs" style="margin:0 0 .6rem">miltongonzalez74@gmail.com</p>
|
||
<div class="flyout-foot"><button class="btn sm" onclick="closeFlyouts()">Perfil</button><button class="btn sm danger" onclick="dlgLogout()">Cerrar sesión</button></div>
|
||
</div>
|
||
|
||
<div class="teaching-tip" id="teachingTip">
|
||
<h4><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M12 3l10 18H2L12 3z"/><path d="M12 10v4M12 17h.01"/></svg>Nuevo: clic derecho</h4>
|
||
<p class="muted xs" style="margin:0 0 .6rem">Ahora puedes hacer clic derecho sobre cualquier fila para ver acciones rápidas.</p>
|
||
<div class="flyout-foot"><button class="btn sm accent" onclick="hideTeachingTip()">Entendido</button></div>
|
||
</div>
|
||
|
||
<div class="toast-stack" id="toastStack"></div>
|
||
|
||
<script src="lucide.min.js"></script>
|
||
<script src="chart.umd.js"></script>
|
||
<script>
|
||
const root=document.documentElement, body=document.body;
|
||
/* ---------- región/componente nombres ---------- */
|
||
function toggleNames(){body.classList.toggle('show-names'); closeAllMenus();}
|
||
|
||
/* ---------- tema / acento / idioma ---------- */
|
||
const ACCENTS=[['blue','#0078D4'],['purple',' #8764B8'],['green','#10893E'],['red','#C42B1C'],['orange','#CA5010'],['teal','#009984']];
|
||
function paintSwatches(id){const h=document.getElementById(id); if(!h)return; h.innerHTML='';
|
||
ACCENTS.forEach(([k,hex])=>{const b=document.createElement('button');
|
||
b.className='btn icon-only sm tip'; b.setAttribute('data-tip',k);
|
||
b.style.background=hex.trim(); b.style.borderColor=hex.trim();
|
||
b.onclick=()=>setAccent(k); h.appendChild(b);});}
|
||
function setAccent(a){root.dataset.accent=a; localStorage.setItem('win.accent',a); buildCharts();}
|
||
function setTheme(t){root.dataset.theme=t; localStorage.setItem('win.theme',t); buildCharts();}
|
||
function setLang(l){localStorage.setItem('win.lang',l); toastInfo(l==='en'?'Language changed':'Idioma cambiado', l==='en'?'The interface is now in English.':'La interfaz ahora está en español.');}
|
||
|
||
/* ---------- navegación (expandida/compacta) ---------- */
|
||
function toggleNav(){root.dataset.navmode = root.dataset.navmode==='compact' ? 'expanded':'compact'; localStorage.setItem('win.navmode',root.dataset.navmode);}
|
||
function filterNav(){const q=document.getElementById('navSearch').value.toLowerCase();
|
||
document.querySelectorAll('.item-nav').forEach(a=>{a.style.display=a.textContent.toLowerCase().includes(q)?'':'none';});}
|
||
|
||
const TITULOS={resumen:'Resumen',dashboard:'Dashboard',listado:'Listado',maestrodetalle:'Maestro-detalle',formulario:'Formulario',dialogos:'Diálogos y avisos',menus:'Menús y flyouts',arbol:'Árbol / carpetas',config:'Configuración',plantillas:'Plantillas',iconos:'Íconos'};
|
||
function showView(v,el){
|
||
document.querySelectorAll('.area-contenido>section').forEach(s=>s.classList.toggle('on',s.dataset.view===v));
|
||
document.querySelectorAll('.item-nav').forEach(a=>a.classList.toggle('active',a.dataset.view===v));
|
||
document.getElementById('tituloVista').textContent=TITULOS[v];
|
||
document.getElementById('migaActual').textContent=TITULOS[v];
|
||
document.getElementById('areaContenido').scrollTop=0;
|
||
closeAllMenus();
|
||
if(v==='dashboard')buildCharts();
|
||
if(v==='listado')renderLV();
|
||
if(v==='maestrodetalle')renderMD();
|
||
if(v==='arbol')renderTree();
|
||
if(v==='iconos')renderIcons();
|
||
if(v==='formulario')buildComboCiudad();
|
||
}
|
||
|
||
/* ---------- cierre global de popups (light-dismiss) ---------- */
|
||
function closeAllMenus(){
|
||
document.querySelectorAll('.menu-flotante.open').forEach(m=>m.classList.remove('open'));
|
||
document.querySelectorAll('.mb-item.open').forEach(m=>m.classList.remove('open'));
|
||
document.querySelectorAll('.combo.open').forEach(m=>m.classList.remove('open'));
|
||
document.querySelectorAll('.dp.open').forEach(m=>m.classList.remove('open'));
|
||
}
|
||
function closeFlyouts(){document.querySelectorAll('.flyout.open').forEach(f=>f.classList.remove('open'));}
|
||
document.addEventListener('click',e=>{
|
||
if(!e.target.closest('.menu-flotante')&&!e.target.closest('.mb-item')&&!e.target.closest('[oncontextmenu]')&&!e.target.closest('.cb-derecha')) closeAllMenus();
|
||
if(!e.target.closest('.flyout')&&!e.target.closest('#cuentaNav')) closeFlyouts();
|
||
if(!e.target.closest('.combo')) document.querySelectorAll('.combo.open').forEach(m=>m.classList.remove('open'));
|
||
if(!e.target.closest('.dp')) document.querySelectorAll('.dp.open').forEach(m=>m.classList.remove('open'));
|
||
});
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeAllMenus();closeFlyouts();closeDlg();hideTeachingTip();}});
|
||
|
||
/* ---------- menú contextual (clic derecho) ---------- */
|
||
const CTX_ITEMS=[
|
||
['eye','Ver detalle',''],['pencil','Editar','Ctrl+E'],['copy','Duplicar','Ctrl+D'],'sep',
|
||
['download','Exportar como','','submenu'],'sepdanger',['trash-2','Eliminar','Del','danger']];
|
||
function buildCtxHtml(rowLabel){
|
||
let h='';
|
||
CTX_ITEMS.forEach(it=>{
|
||
if(it==='sep'){h+='<div class="mi-sep"></div>';return;}
|
||
if(it==='sepdanger'){h+='<div class="mi-sep"></div>';return;}
|
||
const [ico,label,accel,mod]=it;
|
||
if(mod==='submenu'){
|
||
h+=`<div class="mi"><span data-lucide="${ico}" style="width:15px;height:15px"></span><span class="lbl">${label}</span><span class="arrow">▸</span>
|
||
<div class="submenu menu-flotante open"><div class="mi"><span class="lbl">CSV</span></div><div class="mi"><span class="lbl">Excel</span></div><div class="mi"><span class="lbl">PDF</span></div></div></div>`;
|
||
} else {
|
||
h+=`<div class="mi ${mod==='danger'?'danger':''}" onclick="ctxAction('${label}','${rowLabel}')"><span data-lucide="${ico}" style="width:15px;height:15px"></span><span class="lbl">${label}</span><span class="accel">${accel}</span></div>`;
|
||
}
|
||
});
|
||
return h;
|
||
}
|
||
function openCtx(x,y,rowLabel){
|
||
const m=document.getElementById('ctxMenu'); m.innerHTML=buildCtxHtml(rowLabel);
|
||
m.style.left='0px'; m.style.top='0px'; m.classList.add('open');
|
||
lucide.createIcons();
|
||
const r=m.getBoundingClientRect();
|
||
let left=x, top=y;
|
||
if(left+r.width>innerWidth-8) left=innerWidth-r.width-8;
|
||
if(top+r.height>innerHeight-8) top=innerHeight-r.height-8;
|
||
m.style.left=left+'px'; m.style.top=top+'px';
|
||
}
|
||
function openCtxDemo(e){e.preventDefault(); closeAllMenus(); openCtx(e.clientX,e.clientY,'elemento de ejemplo'); return false;}
|
||
function ctxAction(action,row){closeAllMenus(); toastInfo(action, `Sobre "${row}".`);}
|
||
|
||
/* ---------- menú de barra (Archivo/Editar/Ver) ---------- */
|
||
function toggleMbMenu(e,el,id){e.stopPropagation(); const open=el.classList.contains('open'); closeAllMenus();
|
||
if(!open){el.classList.add('open'); document.getElementById(id).classList.add('open');}}
|
||
function toggleMenu(e,id,anchor){e.stopPropagation(); const m=document.getElementById(id); const open=m.classList.contains('open'); closeAllMenus();
|
||
if(!open){const r=anchor.getBoundingClientRect(); m.style.position='fixed'; m.style.left=(r.right-210)+'px'; m.style.top=(r.bottom+4)+'px'; m.classList.add('open'); lucide.createIcons();}}
|
||
|
||
/* ---------- flyout ---------- */
|
||
function toggleFlyout(e,anchorId,flyId){e.stopPropagation(); const f=document.getElementById(flyId); const open=f.classList.contains('open'); closeFlyouts();
|
||
if(!open){const r=document.getElementById(anchorId).getBoundingClientRect(); f.style.left=(r.right+8)+'px'; f.style.top=(r.top-40)+'px'; f.classList.add('open');}}
|
||
|
||
/* ---------- ContentDialog ---------- */
|
||
const TONE_ICO={info:'info',success:'circle-check',warning:'triangle-alert',error:'circle-x'};
|
||
const TONE_LBL={info:'Información',success:'Listo',warning:'Atención',error:'Error'};
|
||
function openDialog({tone='info',title='',message='',actions}={}){
|
||
document.getElementById('cdBar').className='cd-bar '+tone;
|
||
document.getElementById('cdTitle').className='cd-title '+tone;
|
||
document.getElementById('cdTitle').innerHTML=`<span data-lucide="${TONE_ICO[tone]}"></span>${title||TONE_LBL[tone]}`;
|
||
document.getElementById('cdMsg').textContent=message;
|
||
const foot=document.getElementById('cdFoot'); foot.innerHTML='';
|
||
(actions||[{label:'Cerrar',variant:'accent'}]).forEach(a=>{
|
||
const b=document.createElement('button'); b.className='btn '+(a.variant==='primary'||a.variant==='accent'?'accent':a.variant==='danger'?'danger':'');
|
||
b.textContent=a.label; b.onclick=()=>{a.onClick&&a.onClick(); if(!a.keepOpen)closeDlg();}; foot.appendChild(b);});
|
||
document.getElementById('dlgOverlay').classList.add('open'); lucide.createIcons();
|
||
}
|
||
function closeDlg(){document.getElementById('dlgOverlay').classList.remove('open');}
|
||
function dlg(t){openDialog({tone:t,message:t==='error'?'No se pudo completar la operación. Intenta nuevamente.':t==='warning'?'Revisa los datos antes de continuar.':t==='success'?'Los cambios se guardaron correctamente.':'Esta es una notificación informativa de ejemplo.'});}
|
||
function dlgAceptar(){openDialog({tone:'info',title:'¿Confirmas la acción?',message:'Esta acción no se puede deshacer.',
|
||
actions:[{label:'Cancelar',variant:'standard'},{label:'Aceptar',variant:'accent'}]});}
|
||
function dlgEliminar(){openDialog({tone:'warning',title:'¿Eliminar el elemento seleccionado?',message:'CLI-002 — Juan Pérez se eliminará permanentemente.',
|
||
actions:[{label:'Cancelar',variant:'standard'},{label:'Eliminar',variant:'danger',onClick:()=>toastInfo('Eliminado','El registro se eliminó correctamente.')}]});}
|
||
function dlgLogout(){closeFlyouts(); openDialog({tone:'warning',title:'¿Cerrar sesión?',message:'Se cerrará tu sesión actual en este equipo.',
|
||
actions:[{label:'Cancelar',variant:'standard'},{label:'Cerrar sesión',variant:'danger'}]});}
|
||
function dlgNuevo(){openDialog({tone:'info',title:'Nuevo registro',message:'Aquí se mostraría el formulario de alta (patrón: formulario embebido o ventana secundaria).',actions:[{label:'Entendido',variant:'accent'}]});}
|
||
|
||
/* ---------- InfoBar dinámico ---------- */
|
||
function addInfoBar(){
|
||
const host=document.getElementById('infobarHost'); const d=document.createElement('div'); d.className='infobar success';
|
||
d.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><path d="M20 6L9 17l-5-5"/></svg><div class="ib-txt"><b>Operación completada</b>El proceso terminó sin errores.</div><button class="ib-x" onclick="this.closest(\'.infobar\').remove()">✕</button>';
|
||
host.appendChild(d);
|
||
}
|
||
|
||
/* ---------- Toast ---------- */
|
||
function toastInfo(title,msg){
|
||
const stack=document.getElementById('toastStack'); const t=document.createElement('div'); t.className='toast';
|
||
t.innerHTML=`<svg viewBox="0 0 24 24" fill="none" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 12h1v5h1"/></svg><div><b>${title}</b><span>${msg}</span></div>`;
|
||
stack.appendChild(t); setTimeout(()=>{t.style.transition='opacity .2s'; t.style.opacity='0'; setTimeout(()=>t.remove(),200);},4000);
|
||
}
|
||
|
||
/* ---------- Teaching tip ---------- */
|
||
function showTeachingTip(){const a=document.getElementById('ttAnchor'), tt=document.getElementById('teachingTip');
|
||
const r=a.getBoundingClientRect(); tt.style.left=r.left+'px'; tt.style.top=(r.bottom+12)+'px'; tt.classList.add('open'); lucide.createIcons();}
|
||
function hideTeachingTip(){document.getElementById('teachingTip').classList.remove('open');}
|
||
|
||
/* ---------- ComboBox ---------- */
|
||
function toggleCombo(e,id){e.stopPropagation(); const el=document.getElementById(id); const open=el.classList.contains('open'); closeAllMenus(); if(!open)el.classList.add('open');}
|
||
function setCombo(id,li,label){document.getElementById(id).classList.remove('open');
|
||
if(id==='cbEstado'){document.getElementById('cbEstadoLbl').textContent=label; lvEstado=label; renderLV();}
|
||
document.querySelectorAll('#'+id+' .combo-opt').forEach(o=>o.classList.toggle('sel',o===li));}
|
||
function comboFilter(input){const q=input.value.toLowerCase();
|
||
input.parentElement.querySelectorAll('.combo-opt').forEach(o=>o.classList.toggle('hidden',!o.textContent.toLowerCase().includes(q)));}
|
||
const CIUDADES=['Quito','Guayaquil','Cuenca','Ambato','Manta','Loja','Riobamba','Machala','Ibarra','Portoviejo','Esmeraldas','Latacunga'];
|
||
function buildComboCiudad(){const h=document.getElementById('cbCiudadOpts'); if(h.dataset.built)return; h.dataset.built='1';
|
||
h.innerHTML=CIUDADES.map(c=>`<div class="combo-opt" onclick="pickCiudad(this,'${c}')">${c}</div>`).join('');}
|
||
function pickCiudad(li,c){document.getElementById('cbCiudadLbl').textContent=c; document.getElementById('cbCiudad').classList.remove('open');
|
||
document.querySelectorAll('#cbCiudad .combo-opt').forEach(o=>o.classList.toggle('sel',o===li));}
|
||
|
||
/* ---------- NumberBox ---------- */
|
||
function numStep(btn,dir){const input=btn.parentElement.querySelector('input'); let v=parseInt(input.value||'0')+dir; if(v<0)v=0; input.value=v;}
|
||
|
||
/* ---------- DatePicker ---------- */
|
||
const DOWS=['L','M','X','J','V','S','D']; let dpState={};
|
||
function iso(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}
|
||
function fmt(d){return String(d.getDate()).padStart(2,'0')+'/'+String(d.getMonth()+1).padStart(2,'0')+'/'+d.getFullYear();}
|
||
function toggleDP(e,id){e.stopPropagation(); const el=document.getElementById(id); const open=el.classList.contains('open'); closeAllMenus();
|
||
if(!open){if(!dpState[id])dpState[id]={view:new Date(),sel:null}; el.classList.add('open'); renderDP(id);}}
|
||
function dpNav(id,dir){dpState[id].view.setMonth(dpState[id].view.getMonth()+dir); renderDP(id);}
|
||
function renderDP(id){const st=dpState[id], v=st.view, y=v.getFullYear(), m=v.getMonth();
|
||
document.getElementById(id+'Mes').textContent=v.toLocaleDateString('es-EC',{month:'long',year:'numeric'});
|
||
const first=new Date(y,m,1); let startDay=(first.getDay()+6)%7; const daysInMonth=new Date(y,m+1,0).getDate();
|
||
const today=new Date(); let html=DOWS.map(d=>`<span class="dow">${d}</span>`).join('');
|
||
for(let i=0;i<startDay;i++)html+='<button class="out"></button>';
|
||
for(let d=1;d<=daysInMonth;d++){const dt=new Date(y,m,d); const isToday=iso(dt)===iso(today); const isSel=st.sel&&iso(dt)===iso(st.sel);
|
||
html+=`<button class="${isToday?'today':''} ${isSel?'sel':''}" onclick="dpPick('${id}',${y},${m},${d})">${d}</button>`;}
|
||
document.getElementById(id+'Grid').innerHTML=html;
|
||
}
|
||
function dpPick(id,y,m,d){const dt=new Date(y,m,d); dpState[id].sel=dt; document.getElementById(id+'Lbl').textContent=fmt(dt);
|
||
document.getElementById(id).classList.remove('open');}
|
||
|
||
/* ---------- ListView demo ---------- */
|
||
const CLIENTES=[
|
||
{cod:'CLI-001',nombre:'Ana Torres',monto:1250.00,estado:'Activo'},{cod:'CLI-002',nombre:'Juan Pérez',monto:340.50,estado:'Pendiente'},
|
||
{cod:'CLI-003',nombre:'María Ríos',monto:980.00,estado:'Activo'},{cod:'CLI-004',nombre:'Carlos Mora',monto:0,estado:'Inactivo'},
|
||
{cod:'CLI-005',nombre:'Lucía Vega',monto:2210.75,estado:'Activo'},{cod:'CLI-006',nombre:'Pedro Salas',monto:150.00,estado:'Pendiente'},
|
||
{cod:'CLI-007',nombre:'Sofía Cruz',monto:670.30,estado:'Activo'},{cod:'CLI-008',nombre:'Diego León',monto:0,estado:'Inactivo'},
|
||
{cod:'CLI-009',nombre:'Elena Paz',monto:1420.00,estado:'Activo'},{cod:'CLI-010',nombre:'Iván Soto',monto:95.20,estado:'Pendiente'},
|
||
{cod:'CLI-011',nombre:'Rosa Núñez',monto:530.00,estado:'Activo'},{cod:'CLI-012',nombre:'Marco Silva',monto:0,estado:'Inactivo'},
|
||
];
|
||
let lvSort_='cod', lvDir=1, lvEstado='Todos', lvPage=1, lvSel=new Set();
|
||
function lvSort(k){lvDir = lvSort_===k ? -lvDir : 1; lvSort_=k; renderLV();}
|
||
function lvToggleAll(chk){document.querySelectorAll('#lvBody .lv-chk').forEach(c=>{c.checked=chk.checked; const id=c.dataset.id; if(chk.checked)lvSel.add(id); else lvSel.delete(id);}); lvUpdateSelInfo();}
|
||
function lvUpdateSelInfo(){document.getElementById('lvSelInfo').textContent = lvSel.size? `${lvSel.size} de ${CLIENTES.length} seleccionados` : '';
|
||
document.getElementById('statusSel').textContent = lvSel.size? `${lvSel.size} seleccionados` : '';}
|
||
function pageWindow(page,pages){let start=page-2,end=page+2; if(start<1){end+=1-start;start=1;} if(end>pages){start-=end-pages;end=pages;} start=Math.max(1,start); return {start,end};}
|
||
function renderLV(){
|
||
const q=(document.getElementById('lvSearch').value||'').toLowerCase();
|
||
let rows=CLIENTES.filter(c=>(lvEstado==='Todos'||c.estado===lvEstado) && (c.cod+c.nombre).toLowerCase().includes(q));
|
||
rows.sort((a,b)=>{let r = a[lvSort_]>b[lvSort_]?1:a[lvSort_]<b[lvSort_]?-1:0; return r*lvDir;});
|
||
document.querySelectorAll('table.lv th[data-k]').forEach(th=>th.classList.toggle('sorted',th.dataset.k===lvSort_));
|
||
const perRaw=document.getElementById('lvPerPage').value; const per = perRaw==='Todo'?rows.length||1:parseInt(perRaw);
|
||
const pages=Math.max(1,Math.ceil(rows.length/per)); if(lvPage>pages)lvPage=pages;
|
||
const startIdx=(lvPage-1)*per; const pageRows=rows.slice(startIdx,startIdx+per);
|
||
const badgeCls={Activo:'success',Pendiente:'warning',Inactivo:'danger'};
|
||
let html=pageRows.map(c=>`<tr class="${lvSel.has(c.cod)?'sel':''}" onclick="lvRowClick(event,'${c.cod}')" oncontextmenu="return lvCtx(event,'${c.nombre}')">
|
||
<td onclick="event.stopPropagation()"><input type="checkbox" class="lv-chk" data-id="${c.cod}" ${lvSel.has(c.cod)?'checked':''} onchange="lvChk(this,'${c.cod}')"></td>
|
||
<td>${c.cod}</td><td>${c.nombre}</td><td>$${c.monto.toFixed(2)}</td>
|
||
<td><span class="badge ${badgeCls[c.estado]}"><i class="dot"></i>${c.estado}</span></td>
|
||
<td onclick="event.stopPropagation()"><button class="btn icon-only sm" onclick="lvCtxBtn(event,this,'${c.nombre}')"><svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="5" r="1.3" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.3" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="1.3" fill="currentColor" stroke="none"/></svg></button></td></tr>`).join('');
|
||
for(let i=pageRows.length;i<per && per<=50;i++) html+='<tr class="filler"><td colspan="6">•</td></tr>';
|
||
document.getElementById('lvBody').innerHTML=html || '<tr><td colspan="6" class="muted" style="text-align:center;padding:2rem">Sin resultados</td></tr>';
|
||
document.getElementById('lvRango').textContent = rows.length? `Mostrando ${startIdx+1}–${Math.min(startIdx+per,rows.length)} de ${rows.length}` : 'Sin resultados';
|
||
const pag=document.getElementById('lvPag'); pag.innerHTML='';
|
||
if(pages>1){
|
||
const prev=document.createElement('button'); prev.textContent='‹'; prev.disabled=lvPage===1; prev.onclick=()=>{lvPage--;renderLV();}; pag.appendChild(prev);
|
||
const {start,end}=pageWindow(lvPage,pages);
|
||
for(let p=start;p<=end;p++){const b=document.createElement('button'); b.textContent=p; if(p===lvPage)b.classList.add('act'); b.onclick=()=>{lvPage=p;renderLV();}; pag.appendChild(b);}
|
||
const next=document.createElement('button'); next.textContent='›'; next.disabled=lvPage===pages; next.onclick=()=>{lvPage++;renderLV();}; pag.appendChild(next);
|
||
}
|
||
lucide.createIcons();
|
||
}
|
||
function lvChk(chk,id){if(chk.checked)lvSel.add(id); else lvSel.delete(id); chk.closest('tr').classList.toggle('sel',chk.checked); lvUpdateSelInfo();}
|
||
function lvRowClick(e,id){const chk=e.currentTarget.querySelector('.lv-chk'); chk.checked=!chk.checked; lvChk(chk,id);}
|
||
function lvCtx(e,nombre){e.preventDefault(); closeAllMenus(); openCtx(e.clientX,e.clientY,nombre); return false;}
|
||
function lvCtxBtn(e,btn,nombre){e.stopPropagation(); const r=btn.getBoundingClientRect(); closeAllMenus(); openCtx(r.left,r.bottom+4,nombre);}
|
||
|
||
/* ---------- Maestro-detalle ---------- */
|
||
let mdSel=CLIENTES[0].cod;
|
||
function renderMD(){
|
||
document.getElementById('mdList').innerHTML=CLIENTES.map(c=>`<div class="split-item ${c.cod===mdSel?'sel':''}" onclick="mdPick('${c.cod}')"><b>${c.nombre}</b><span>${c.cod} · $${c.monto.toFixed(2)}</span></div>`).join('');
|
||
const c=CLIENTES.find(x=>x.cod===mdSel);
|
||
document.getElementById('mdDetalle').innerHTML=`<h2>${c.nombre}</h2><p class="muted xs">${c.cod}</p>
|
||
<div class="grid-2" style="margin-top:.8rem">
|
||
<div class="field"><label>Estado</label><span class="badge ${c.estado==='Activo'?'success':c.estado==='Pendiente'?'warning':'danger'}" style="width:fit-content"><i class="dot"></i>${c.estado}</span></div>
|
||
<div class="field"><label>Saldo</label><b>$${c.monto.toFixed(2)}</b></div>
|
||
</div>
|
||
<div class="row-wrap" style="margin-top:1rem"><button class="btn sm accent">Editar</button><button class="btn sm danger" onclick="dlgEliminar()">Eliminar</button></div>`;
|
||
}
|
||
function mdPick(cod){mdSel=cod; renderMD();}
|
||
(function initSplitter(){
|
||
const div=document.getElementById('mdDiv');
|
||
div.addEventListener('mousedown',e=>{
|
||
e.preventDefault(); div.classList.add('dragging');
|
||
const list=document.getElementById('mdList'); const startX=e.clientX; const startW=list.offsetWidth;
|
||
function mm(ev){let w=startW+(ev.clientX-startX); w=Math.max(180,Math.min(460,w)); list.style.width=w+'px';}
|
||
function mu(){div.classList.remove('dragging'); document.removeEventListener('mousemove',mm); document.removeEventListener('mouseup',mu);}
|
||
document.addEventListener('mousemove',mm); document.addEventListener('mouseup',mu);
|
||
});
|
||
})();
|
||
|
||
/* ---------- Treeview ---------- */
|
||
const TREE=[
|
||
{n:'Clientes',ico:'folder',kids:[{n:'Activos',ico:'folder',kids:[{n:'Ana Torres',leaf:true},{n:'María Ríos',leaf:true}]},{n:'Inactivos',ico:'folder',kids:[{n:'Carlos Mora',leaf:true}]}]},
|
||
{n:'Facturación',ico:'folder',kids:[{n:'2026',ico:'folder',kids:[{n:'Enero',leaf:true},{n:'Febrero',leaf:true}]}]},
|
||
{n:'Reportes',ico:'folder',kids:[{n:'Ventas.pdf',leaf:true},{n:'Cartera.xlsx',leaf:true}]},
|
||
];
|
||
function treeHtml(nodes){
|
||
return '<ul class="tree">'+nodes.map(n=>{
|
||
if(n.leaf) return `<li class="leaf"><div class="node" onclick="treeSel(this)"><span class="twig"></span><span class="ico" data-lucide="file-text"></span><span>${n.n}</span></div></li>`;
|
||
return `<li><div class="node" onclick="treeToggle(this)"><span class="twig" data-lucide="chevron-down"></span><span class="ico" data-lucide="folder"></span><span>${n.n}</span></div>${treeHtml(n.kids)}</li>`;
|
||
}).join('')+'</ul>';
|
||
}
|
||
function renderTree(){document.getElementById('treeRoot').outerHTML=treeHtml(TREE).replace('<ul class="tree">','<ul class="tree" id="treeRoot">'); lucide.createIcons();}
|
||
function treeToggle(el){el.closest('li').classList.toggle('collapsed');}
|
||
function treeSel(el){document.querySelectorAll('.tree .node.sel').forEach(n=>n.classList.remove('sel')); el.classList.add('sel');}
|
||
|
||
/* ---------- Charts ---------- */
|
||
let chLine,chDon;
|
||
function cssVar(n){return 'rgb('+getComputedStyle(root).getPropertyValue(n).trim()+')';}
|
||
function buildCharts(){
|
||
const ctx1=document.getElementById('chLine'), ctx2=document.getElementById('chDon');
|
||
if(!ctx1||typeof Chart==='undefined')return;
|
||
if(chLine)chLine.destroy(); if(chDon)chDon.destroy();
|
||
const grid=cssVar('--line'), txt=cssVar('--muted');
|
||
chLine=new Chart(ctx1,{type:'line',data:{labels:['Ene','Feb','Mar','Abr','May','Jun'],datasets:[{data:[12,19,14,22,28,25],borderColor:cssVar('--accent'),backgroundColor:cssVar('--accent'),tension:.35,pointRadius:3}]},
|
||
options:{maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{grid:{color:grid},ticks:{color:txt}},y:{grid:{color:grid},ticks:{color:txt}}}}});
|
||
chDon=new Chart(ctx2,{type:'doughnut',data:{labels:['Activo','Pendiente','Inactivo'],datasets:[{data:[62,23,15],backgroundColor:[cssVar('--success'),cssVar('--warning'),cssVar('--danger')]}]},
|
||
options:{maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{color:txt}}}}});
|
||
}
|
||
|
||
/* ---------- Icon gallery ---------- */
|
||
const ICON_LIB=['home','layout-grid','layout-dashboard','menu','search','filter','settings','sliders-horizontal','plus','minus','x','check','pencil','trash-2','save','download','upload','copy','printer','share-2','refresh-cw','rotate-ccw','lock','unlock','key','shield','log-out','power','ellipsis','ellipsis-vertical','maximize-2','minimize-2','move','arrow-up','arrow-down','arrow-left','arrow-right','chevron-down','chevron-right','external-link','user','users','user-plus','contact','file','file-text','folder','folder-open','archive','book-open','bookmark','package','box','shopping-cart','store','receipt','tag','dollar-sign','credit-card','wallet','landmark','calculator','chart-column','chart-line','chart-pie','trending-up','activity','target','mail','send','inbox','bell','phone','calendar','clock','history','map','map-pin','compass','flag','image','camera','video','play','pause','star','heart','eye','sun','moon','cloud','info','circle-alert','triangle-alert','circle-check','circle-x','database','server','terminal','code','wrench','building','briefcase','table','list','kanban','columns-2','app-window','layout-panel-left','shapes','wifi','globe'];
|
||
let icoPage=1;
|
||
function renderIcons(){
|
||
const q=(document.getElementById('icoSearch').value||'').toLowerCase();
|
||
const list=ICON_LIB.filter(n=>n.includes(q));
|
||
const per=30, pages=Math.max(1,Math.ceil(list.length/per)); if(icoPage>pages)icoPage=pages;
|
||
const start=(icoPage-1)*per, pageItems=list.slice(start,start+per);
|
||
document.getElementById('icoGallery').innerHTML=pageItems.map(n=>`<div class="icon-tile tip" data-tip="${n}"><span data-lucide="${n}"></span><span class="ico-name">${n}</span></div>`).join('');
|
||
document.getElementById('icoRango').textContent = list.length? `Mostrando ${start+1}–${Math.min(start+per,list.length)} de ${list.length}` : 'Sin resultados';
|
||
const pag=document.getElementById('icoPag'); pag.innerHTML='';
|
||
if(pages>1){const prev=document.createElement('button'); prev.textContent='‹'; prev.disabled=icoPage===1; prev.onclick=()=>{icoPage--;renderIcons();}; pag.appendChild(prev);
|
||
const {start:s,end:e}=pageWindow(icoPage,pages);
|
||
for(let p=s;p<=e;p++){const b=document.createElement('button'); b.textContent=p; if(p===icoPage)b.classList.add('act'); b.onclick=()=>{icoPage=p;renderIcons();}; pag.appendChild(b);}
|
||
const next=document.createElement('button'); next.textContent='›'; next.disabled=icoPage===pages; next.onclick=()=>{icoPage++;renderIcons();}; pag.appendChild(next);}
|
||
lucide.createIcons();
|
||
}
|
||
|
||
/* ---------- init ---------- */
|
||
(function restorePrefs(){
|
||
const th=localStorage.getItem('win.theme'); if(th)root.dataset.theme=th;
|
||
const ac=localStorage.getItem('win.accent'); if(ac)root.dataset.accent=ac;
|
||
const nm=localStorage.getItem('win.navmode'); if(nm)root.dataset.navmode=nm;
|
||
})();
|
||
paintSwatches('accentSwatches'); paintSwatches('accentSwatches2');
|
||
lucide.createIcons();
|
||
renderLV(); renderTree();
|
||
setTimeout(buildCharts,60);
|
||
</script>
|
||
</body>
|
||
</html>
|