Files
project.linkedcorp.net/docs/UI-UX-WINDOWS.html
T
Milton Gonzalez 1d22adc42e chore: estructura inicial del MVP
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>
2026-09-03 08:37:58 -05:00

1266 lines
92 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>