assetUrl(+i));
el.remove();
return html;
}
function releaseAssets(){
if(!document.getElementById('tfSrcDM')&&!document.getElementById('tfSrcFUNNEL')){
assetIndex=null;assetBytes=null;
document.getElementById('tfAssetIndex')?.remove();document.getElementById('tfAssetData')?.remove();
}
}
let dmGuideUrl=null;
function getGuideUrl(){
if(!dmGuideUrl){const html=readApp('tfSrcGUIDE');dmGuideUrl=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'}));}
return dmGuideUrl;
}
/* La Guía va al final del archivo: si todavía no se terminó de leer, se espera a DOMContentLoaded. */
function withGuide(fn){
if(!dmGuideUrl&&!document.getElementById('tfSrcGUIDE')&&document.readyState==='loading')document.addEventListener('DOMContentLoaded',fn,{once:true});
else fn();
}
/* V19 · "Guía de DMs" abre la misma Guía dentro de la app (Templates): así "Usar Plantilla" copia el flujo
a un canvas nuevo en vez de descargar el XLSX. La Guía en ventana aparte queda para abrir el archivo suelto. */
window.tfOpenDmGuide=function(){go('templates')};
window.tfOpenDmGuideWindow=function(){
withGuide(()=>{try{window.open(getGuideUrl(),'_blank','noopener')}catch(err){console.error('[Guía de DMs]',err)}});
};
/* ==================================================================================================
V19 · ARQUITECTURA DE LA APP
Business Brain es la app principal. El shell muestra una sola vista a la vez:
bb:offers | bb:knowledge → Business Brain (iframe srcdoc, conocimiento reutilizable del negocio)
templates → Guía / biblioteca de Templates + Crear con IA (iframe blob, se carga al abrirla)
dm → DM Mapping (Message Mapping: donde termina todo Flow)
funnel → Funnel Mapping (extra)
Cada app se carga recién cuando se abre (o en segundo plano cuando el usuario está inactivo, solo DM).
Las apps no se hablan entre sí: todo pasa por los mensajes de este shell (ver el listener al final).
================================================================================================== */
const frames={dm,funnel,bb};
const SRC={dm:'tfSrcDM',funnel:'tfSrcFUNNEL',bb:'tfSrcBB'};
const toolState={dm:0,funnel:0,bb:0}; // 0 sin cargar · 1 cargando · 2 lista
const waiters={dm:[],funnel:[],bb:[]};
function loadTool(t){
if(toolState[t])return;
if(window.tfAuthGate&&!window.tfAuthReady){window.tfAuthGate.then(()=>loadTool(t));return} /* V24 · nada carga antes del acceso */
toolState[t]=1;
frames[t].addEventListener('load',()=>{
toolState[t]=2;mark(t+':ready');
watchActivity(frames[t]);
if(t==='bb')sendChrome(); /* ☰ del Business Brain según el estado de la barra */
waiters[t].splice(0).forEach(fn=>{try{fn()}catch(err){console.error(err)}});
},{once:true});
/* Business Brain no usa el bloque de imágenes compartidas de DM y Funnel */
const go=()=>(t==='bb'?Promise.resolve():prepareAssets()).then(()=>{
mark(t+':assets');
frames[t].srcdoc=readApp(SRC[t]);
mark(t+':srcdoc');
if(t!=='bb')releaseAssets();
});
if(!document.getElementById(SRC[t])&&document.readyState==='loading')document.addEventListener('DOMContentLoaded',go,{once:true});
else go();
}
function whenReady(t,fn){
if(toolState[t]===2){fn();return;}
waiters[t].push(fn);loadTool(t);
}
function toDm(msg,delay){
whenReady('dm',()=>setTimeout(()=>{try{dm.contentWindow.postMessage(msg,'*')}catch(_e){}},delay||0));
}
function toBb(msg){whenReady('bb',()=>{try{bb.contentWindow.postMessage(msg,'*')}catch(_e){}})}
let lastInput=performance.now();
function markInput(){lastInput=performance.now()}
function watchActivity(frame){
try{
const w=frame.contentWindow;
['pointerdown','pointermove','wheel','keydown'].forEach(t=>w.addEventListener(t,markInput,{passive:true,capture:true}));
}catch(_e){}
}
['pointerdown','pointermove','wheel','keydown'].forEach(t=>addEventListener(t,markInput,{passive:true,capture:true}));
function preloadWhenIdle(t){
if(toolState[t])return;
const quiet=performance.now()-lastInput;
if(quiet<1800){setTimeout(()=>preloadWhenIdle(t),1900-quiet);return;}
if('requestIdleCallback' in window)requestIdleCallback(()=>{if(performance.now()-lastInput<1800)preloadWhenIdle(t);else loadTool(t)},{timeout:1500});
else loadTool(t);
}
function ensureTemplates(){
if(templatesLoaded)return;
templatesLoaded=true;
if(templatesLoader)templatesLoader.classList.remove('hide');
withGuide(()=>{templatesFrame.src=getGuideUrl()});
}
/* La Guía trae su propio "Volver al canvas": al terminar de cargar, se oculta el header del overlay. */
templatesFrame?.addEventListener('load',()=>{templatesLoader?.classList.add('hide');templates.classList.add('tf-guide-ready');sendChrome()});
templatesClose?.addEventListener('click',()=>go('dm'));
/* ---------- navegación ---------- */
const nav=document.getElementById('tfNav');
/* bb:offers (Oferta de valor) vive dentro de Bases de Conocimiento: en la barra se marca Bases. */
const LABEL={'bb:knowledge':'Bases de Conocimiento','bb:offers':'Oferta de valor',templates:'Templates',dm:'DM Mapping',funnel:'Funnel Mapping'};
const NAV_OF={'bb:offers':'bb:knowledge',templates:'dm'}; /* V23 · Templates vive dentro de DM Mapping */
function normView(v){v=String(v||'');return LABEL[v]?v:'bb:knowledge'}
function lastCanvas(){try{return localStorage.getItem(KEY)==='funnel'?'funnel':'dm'}catch(_e){return 'dm'}}
function homeView(){try{const v=localStorage.getItem(HOME_KEY);if(v&&LABEL[v]&&v.startsWith('bb:'))return v}catch(_e){}return 'bb:knowledge'} /* V23 · "Atrás" vuelve siempre al Business Brain */
const small=()=>matchMedia('(max-width:820px)').matches;
function openNav(){document.documentElement.classList.add('tf-nav-open')}
function closeNav(){document.documentElement.classList.remove('tf-nav-open')}
/* Sidebar escondida (escritorio). En celular la barra es un menú que se abre y se cierra. */
let navHidden=false;try{navHidden=localStorage.getItem(NAV_KEY)==='1'}catch(_e){}
function applyNav(){
const tool=!!current&&(current==='dm'||current==='funnel');
document.documentElement.classList.toggle('tf-nav-tool',tool);
document.documentElement.classList.toggle('tf-nav-hidden',!tool&&navHidden);
}
function setNavHidden(h,save=true){
navHidden=!!h;
if(save)try{localStorage.setItem(NAV_KEY,navHidden?'1':'0')}catch(_e){}
applyNav();sendChrome();
}
/* Templates sabe si se abrió desde el canvas ("Volver al canvas") y si tiene que mostrar ☰ (barra escondida). */
let tplFromCanvas=false;
function sendChrome(){
const msg={type:'tf-suite-nav-state',hidden:navHidden,back:tplFromCanvas};
try{if(templatesLoaded)templatesFrame.contentWindow.postMessage(Object.assign({},msg,{type:'tf-guide-chrome'}),'*')}catch(_e){}
if(toolState.bb===2)try{bb.contentWindow.postMessage(msg,'*')}catch(_e){}
if(templatesClose)templatesClose.hidden=!tplFromCanvas;
}
nav.addEventListener('click',e=>{
const b=e.target.closest('[data-go]');if(b){closeNav();go(b.dataset.go);return}
if(e.target.closest('[data-nav-hide]')){setNavHidden(true);return}
if(e.target.closest('[data-nav-close]'))closeNav();
});
document.getElementById('tfNavBackdrop')?.addEventListener('click',closeNav);
let current=null,pending=null;
function frameOf(v){return v.startsWith('bb')?'bb':(v==='dm'||v==='funnel')?v:null}
function go(v,remember=true,opts){
v=normView(v);
if(remember)try{localStorage.setItem(VIEW_KEY,v);if(v==='dm'||v==='funnel')localStorage.setItem(KEY,v);else if(v.startsWith('bb:'))localStorage.setItem(HOME_KEY,v)}catch(_e){}
if(v.startsWith('bb:'))toBb({type:'tf-bb-go',view:v.slice(3)});
const t=frameOf(v);
if(t&&toolState[t]!==2){
/* Si todavía no cargó, se mantiene la vista actual y se cambia apenas esté lista. */
pending=v;document.documentElement.style.cursor='progress';
whenReady(t,()=>{if(pending===v){pending=null;document.documentElement.style.cursor='';show(v)}});
return;
}
pending=null;document.documentElement.style.cursor='';
show(v);
}
function show(v){
current=v;
const t=frameOf(v);
for(const k of ['dm','funnel','bb'])frames[k].classList.toggle('active',k===t);
const isTpl=v==='templates';
tplFromCanvas=isTpl; /* V23 · Templates siempre se abre desde DM: muestra "Volver Atrás" */
if(isTpl){
ensureTemplates();
setTimeout(()=>{try{templatesFrame.contentWindow.postMessage({type:'tf-guide-refresh-user-templates'},'*')}catch(_e){}},130);
toDm({type:'tf-request-user-templates'},130);
}
templates.classList.toggle('open',isTpl);templates.setAttribute('aria-hidden',isTpl?'false':'true');
applyNav();sendChrome();
/* DM y Funnel: su sidebar toma el lugar de esta barra y respeta el mismo estado (visible o escondida) */
if(t==='dm'||t==='funnel')try{frames[t].contentWindow.postMessage({type:'tf-suite-sidebar',hidden:navHidden},'*')}catch(_e){}
if(t==='funnel'&&toolState.dm===2)toDm({type:'tf-dm-back-clear'}); /* la "← volver al funnel" de DM ya no hace falta */
const navV=NAV_OF[v]||v;
nav.querySelectorAll('[data-go]').forEach(b=>{const on=b.dataset.go===navV;b.classList.toggle('active',on);if(on)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current')});
document.title='Top Funnels · '+LABEL[v];
const frame=t?frames[t]:null;
setTimeout(()=>{
try{frame&&frame.contentWindow.dispatchEvent(new Event('resize'))}catch(_e){}
try{(frame||templatesFrame).contentWindow.focus()}catch(_e){}
},50);
}
/* ---------- arranque ---------- */
let start='bb:knowledge';
try{const v=localStorage.getItem(VIEW_KEY);if(v&&LABEL[v])start=v==='bb:offers'?'bb:knowledge':v}catch(_e){} /* la app arranca en Bases de Conocimiento */
function revealed(){
if(status)status.textContent='WORKSPACE LISTO';
const delay=Math.max(0,450-(performance.now()-started));
setTimeout(()=>{
requestAnimationFrame(()=>{
boot.classList.add('hide');mark('visible');
setTimeout(()=>boot.remove(),480);
markInput();preloadWhenIdle('dm');
});
},delay);
}
const tfGate=window.tfAuthGate||Promise.resolve(); /* V24 · acceso: nada carga hasta que hay sesión y datos */
const startFrame=frameOf(start);
if(startFrame)whenReady(startFrame,()=>{if(!current)show(start);revealed()});
else tfGate.then(()=>{show(start);revealed()});
if(start.startsWith('bb:'))toBb({type:'tf-bb-go',view:start.slice(3)});
/* Deja pintar el loader antes de empezar a parsear la herramienta
(con respaldo por si rAF está pausado: pestaña o iframe todavía ocultos). */
let kicked=false;
const kick=()=>{if(kicked)return;kicked=true;if(startFrame)loadTool(startFrame)};
tfGate.then(()=>{requestAnimationFrame(()=>setTimeout(kick,0));setTimeout(kick,600)});
/* ---------- librerías a pedido (V19) ----------
jsPDF viaja como texto al final del archivo (tfLibJSPDF) y solo se entrega cuando una app exporta un PDF.
El texto se lee una vez y queda en memoria para los siguientes pedidos (Business Brain, Templates).
JSZip ya viaja dentro de DM (V22): se toma de su código, todavía sin abrir (tfSrcDM) o ya cargado (srcdoc). */
const LIBS={jspdf:'tfLibJSPDF'};const libCache={};
function readLib(name){
if(name==='logo'){const l=document.querySelector('.tf-nav-logo');return l?l.src:''} /* logo blanco para los PDF */
if(libCache[name])return libCache[name];
if(name==='jszip'){
const src=document.getElementById('tfSrcDM')?.textContent.replace(TAG_ESC,'')||dm.srcdoc||'';
/* sin escribir la etiqueta de script literal: este código vive dentro de un script del archivo principal */
const m=new RegExp('<'+'script id="vendor-jszip-3-10-1">([\\s\\S]*?)<'+'/script>').exec(src);
return libCache.jszip=m?m[1]:'';
}
const el=LIBS[name]&&document.getElementById(LIBS[name]);if(!el)return '';
libCache[name]=el.textContent.replace(TAG_ESC,'');el.remove();
return libCache[name];
}
function afterParse(fn){if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',fn,{once:true});else fn()}
/* ---------- mensajes entre apps ----------
V22 · solo se aceptan mensajes de las apps de la suite (antes cualquier ventana podía mandar, por ejemplo,
tf-use-dm-template). */
const fromSuite=src=>src===window||[dm,funnel,bb,templatesFrame].some(f=>f&&f.contentWindow===src);
window.addEventListener('message',e=>{
if(!fromSuite(e.source))return;
const d=e.data||{};
if(d.type==='tf-suite-go'){go(d.view);return;}
if(d.type==='tf-suite-lib-request'){const src=e.source;afterParse(()=>{const code=readLib(d.name);try{src.postMessage({type:'tf-suite-lib',requestId:d.requestId,name:d.name,ok:!!code,code},'*')}catch(_e){}});return;}
/* V19 · sidebar única: ☰ del Business Brain / Templates, "← Atrás" y « de DM y Funnel */
if(d.type==='tf-suite-toggle-nav'){if(small())openNav();else setNavHidden(!navHidden);return;}
if(d.type==='tf-suite-nav-hidden'){setNavHidden(!!d.hidden,false);return;} /* DM/Funnel ya guardaron la clave */
if(d.type==='tf-suite-back'){go(homeView());return;}
if(d.type==='tf-suite-switch-tool'){go(d.tool==='funnel'?'funnel':'dm');return;}
if(d.type==='tf-suite-open-templates'){go('templates',true,{fromCanvas:true});return;}
if(d.type==='tf-suite-open-dm-guide'){go('templates',true,{fromCanvas:true});return;}
if(d.type==='tf-suite-close-templates'){go('dm');return;}
if(d.type==='tf-use-dm-template'){go('dm');toDm(d,80);return;}
/* V22 · Funnel › "Flujo de DM": abre DM con ese flujo */
if(d.type==='tf-funnel-open-dm-flow'){go('dm');toDm({type:'tf-dm-open-flow',flowId:d.flowId,back:String(d.from||'Funnel')},80);return;}
if(d.type==='tf-dm-back-to-funnel'){go('funnel');return;}
if(d.type==='tf-guide-request-user-templates'){toDm({type:'tf-request-user-templates'});return;}
if(d.type==='tf-guide-request-dm-folders'){toDm({type:'tf-request-dm-folders',requestId:d.requestId});return;}
/* V19 · Crear con IA: Knowledge Pack del Business Brain, Excel oficial y validación del resultado (DM) */
if(d.type==='tf-guide-request-kb'){toBb({type:'tf-kb-request',requestId:d.requestId});return;}
if(d.type==='tf-kb-pack'){try{templatesFrame.contentWindow.postMessage(d,'*')}catch(_e){};return;}
if(d.type==='tf-guide-build-xlsx'){toDm({type:'tf-dm-build-xlsx',requestId:d.requestId,opts:d.opts});return;}
if(d.type==='tf-guide-deep-validate'){toDm({type:'tf-deep-validate-ai-xlsx',requestId:d.requestId,xlsx:d.xlsx,filename:d.filename,ctx:d.ctx});return;}
if(d.type==='tf-dm-xlsx'||d.type==='tf-dm-deep-validation'){try{templatesFrame.contentWindow.postMessage(d,'*')}catch(_e){};return;}
if(d.type==='tf-dm-folders'){try{templatesFrame.contentWindow.postMessage(d,'*')}catch(_e){};return;}
if(d.type==='tf-user-templates-data'){try{templatesFrame.contentWindow.postMessage(d,'*')}catch(_e){};return;}
if(d.type==='tf-guide-create-template-category'){toDm({type:'tf-create-template-category',name:d.name,color:d.color});return;}
if(d.type==='tf-guide-create-user-template'){go('dm');toDm({type:'tf-open-create-template-current'},90);return;}
if(d.type==='tf-guide-use-user-template'){go('dm');toDm({type:'tf-use-user-template-id',id:d.id},90);return;}
});
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeNav()});
})();