Gamma-like
Editor por slide → gráficos/tablas editables → export PPTX
1/1
Contenido por diapositiva
Vista previa (slide seleccionada)
Tip: selecciona una slide desde las miniaturas.
Editar slide
/* Basic reset */ * { box-sizing: border-box; } html, body { height: 100%; } body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Apple Color Emoji","Segoe UI Emoji"; background: #0b1220; color: #e6eefc; } .topbar{ display:flex; align-items:center; justify-content:space-between; padding: 14px 18px; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(10,16,30,.7); backdrop-filter: blur(10px); position: sticky; top:0; z-index: 5; } .brand{ display:flex; gap:12px; align-items:center; } .logo{ width:36px; height:36px; border-radius:12px; display:flex; align-items:center; justify-content:center; background: linear-gradient(135deg,#6d5efc,#2dd4bf); color:#06121f; font-weight: 900; } .brand-title{ font-weight: 800; letter-spacing: .2px; } .brand-subtitle{ font-size: 12px; color: rgba(230,238,252,.75); margin-top:2px; } .topbar-actions{ display:flex; align-items:center; gap:10px; } .slide-counter{ font-weight: 700; color: rgba(230,238,252,.92); } .layout{ display:grid; grid-template-columns: 360px minmax(420px, 1fr) 360px; gap: 14px; padding: 14px; align-items:start; } .panel{ background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; overflow:hidden; } .panel-header{ display:flex; align-items:center; justify-content:space-between; padding: 12px 12px 10px; border-bottom: 1px solid rgba(255,255,255,.08); } .panel-title{ font-weight: 800; } .textarea{ width:100%; height: 520px; padding: 12px; border: none; outline: none; resize: vertical; background: rgba(0,0,0,.25); color: #e6eefc; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12.5px; line-height: 1.35; } .panel-footer{ padding: 12px; display:flex; align-items:center; justify-content:space-between; gap: 10px; border-top: 1px solid rgba(255,255,255,.08); } .hint{ font-size: 12px; color: rgba(230,238,252,.75); } .stage{ background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 12px; overflow:hidden; } .stage-top{ display:flex; align-items:flex-start; justify-content:space-between; gap: 10px; padding-bottom: 10px; } .stage-title{ font-weight: 800; } .thumbnails{ display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; } .thumb{ width: 46px; height: 34px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.25); display:flex; align-items:center; justify-content:center; cursor:pointer; transition: transform .12s ease, border-color .12s ease, background .12s ease; } .thumb:hover{ transform: translateY(-1px); border-color: rgba(45,212,191,.6); } .thumb.active{ border-color: rgba(109,94,252,.9); background: rgba(109,94,252,.15); box-shadow: 0 0 0 1px rgba(109,94,252,.3) inset; } .slide-frame{ display:flex; align-items:center; justify-content:center; padding: 10px; background: rgba(0,0,0,.18); border-radius: 14px; border: 1px solid rgba(255,255,255,.06); } .ppt-slide{ width: 980px; max-width: 100%; aspect-ratio: 16/9; background: #ffffff; color: #0b1220; border-radius: 12px; overflow:hidden; position:relative; } .ppt-inner{ position:absolute; inset:0; padding: 0; } .sr-only{ position:absolute !important; width:1px; height:1px; overflow:hidden; clip:rect(1px,1px,1px,1px); white-space:nowrap; border:0; padding:0; margin:-1px; } /* Editor */ .editor{ padding: 12px; } .section{ margin-bottom: 14px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.08); background: rgba(0,0,0,.18); } .section-title{ font-weight: 900; margin-bottom: 10px; display:flex; align-items:center; justify-content:space-between; gap: 10px; } .label{ font-size: 12px; color: rgba(230,238,252,.78); margin-bottom: 6px; } .row{ display:flex; gap:10px; } .row > * { flex: 1; } .input, .select{ width: 100%; padding: 10px 10px; border-radius: 12px; background: rgba(255,255,255,.06); color: #e6eefc; border: 1px solid rgba(255,255,255,.10); outline:none; } .input::placeholder{ color: rgba(230,238,252,.45); } .select{ appearance:none; } .textarea-small{ width:100%; min-height: 90px; padding: 10px; border-radius: 12px; background: rgba(255,255,255,.06); color: #e6eefc; border: 1px solid rgba(255,255,255,.10); outline:none; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12.5px; } .kbd{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; padding: 2px 6px; border-radius: 8px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.2); font-size: 12px; } /* Buttons */ .btn{ padding: 10px 12px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); cursor:pointer; font-weight: 800; color: #e6eefc; background: rgba(255,255,255,.06); transition: transform .12s ease, background .12s ease, border-color .12s ease; } .btn:hover{ transform: translateY(-1px); border-color: rgba(45,212,191,.5); } .btn:active{ transform: translateY(0px); } .btn-primary{ background: linear-gradient(135deg, rgba(109,94,252,.95), rgba(45,212,191,.85)); border-color: rgba(255,255,255,.18); color: #06121f; } .btn-ghost{ background: rgba(255,255,255,.04); } .btn-small{ padding: 8px 10px; font-weight: 800; font-size: 12px; border-radius: 10px; } /* Table editor */ .table-wrap{ overflow:auto; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.03); } .grid-table{ width:100%; border-collapse: collapse; min-width: 320px; } .grid-table th, .grid-table td{ border: 1px solid rgba(255,255,255,.12); padding: 6px; text-align:left; } .grid-table th{ background: rgba(255,255,255,.06); font-weight: 900; } .cell-input{ width:100%; border:none; outline:none; background: transparent; color: #e6eefc; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; } .table-actions{ display:flex; gap:8px; margin-top:10px; } .stage-bottom{ display:flex; gap:10px; align-items:center; justify-content:space-between; padding-top: 10px; } .pill{ font-size: 12px; color: rgba(230,238,252,.78); } .pill-warn{ color: #000; background: #fbbf24; border: 1px solid rgba(0,0,0,.2); padding: 7px 10px; border-radius: 999px; font-weight: 900; } /* Slide layout */ .slide-title{ position:absolute; left: 50px; right: 50px; top: 44px; font-size: 34px; font-weight: 900; color:#0b1220; } .slide-subtitle{ position:absolute; left: 50px; right: 50px; top: 84px; font-size: 16px; font-weight: 700; color: rgba(11,18,32,.72); } .slide-body{ position:absolute; left: 70px; top: 140px; width: 520px; font-size: 18px; color:#0b1220; } .slide-body ul{ margin: 0; padding-left: 18px; } .slide-body li{ margin: 8px 0; } .slide-chart{ position:absolute; right: 70px; top: 150px; width: 360px; height: 240px; background: rgba(0,0,0,.02); border-radius: 12px; padding: 8px; border: 1px solid rgba(0,0,0,.06); } .slide-table{ position:absolute; left: 70px; right: 70px; bottom: 70px; background: rgba(0,0,0,.02); border-radius: 12px; padding: 10px; border: 1px solid rgba(0,0,0,.08); } .slide-table table{ width:100%; border-collapse: collapse; font-size: 14px; color:#0b1220; } .slide-table th, .slide-table td{ border: 1px solid rgba(0,0,0,.12); padding: 6px 8px; } .slide-table th{ background: rgba(109,94,252,.14); font-weight: 900; } .slide-has-bullets .slide-body { display:block; } .slide-has-chart .slide-chart { display:block; } .slide-has-table .slide-table { display:block; } @media (max-width: 1400px){ .layout{ grid-template-columns: 340px 1fr 340px; } .ppt-slide{ width: 900px; } .slide-body{ width: 480px; } .slide-chart{ width: 330px; } } /* Buttons in slide (none) */ /* ========================= File: app.js ========================= */ (() => { const $ = (sel) => document.querySelector(sel); const inputSlides = $("#inputSlides"); const btnParse = $("#btnParse"); const btnPrev = $("#btnPrev"); const btnNext = $("#btnNext"); const btnLoadExample = $("#btnLoadExample"); const btnExportPptx = $("#btnExportPptx"); const slideIndexLabel = $("#slideIndexLabel"); const slideCountLabel = $("#slideCountLabel"); const thumbs = $("#thumbs"); const pptSlide = $("#pptSlide"); const editor = $("#editor"); const statusPill = $("#statusPill"); const chartCaptureHost = $("#chartCaptureHost"); const tableCaptureHost = $("#tableCaptureHost"); // App state let slides = []; let selectedIndex = 0; // Default example input syntax const EXAMPLE = `--- slide --- title = Resumen Ejecutivo subtitle = Q2 2026 bullets: - Ingresos +12% vs Q1 - Margen operativo 18.4% - NPS: 54 (↑) chart: type=bar; title=Ingresos por canal; labels=Web,Partners,Store; series=120,85,45 table: columns=KPI,Valor,Variación; rows=Ingresos|250|+12%; Margen|18.4|+1.2pp; NPS|54|+6 --- slide --- title = Proyección bullets: - Escenario base: crecimiento moderado - Costes controlados chart: type=line; title=Evolución; labels=May,Jun,Jul,Ago; series=65,72,78,84 --- slide --- title = Detalle Financiero bullets: - Desglose por rubro table: columns=Rubro,2025,2026; rows=Ventas|180|210; Costes|95|110; Beneficio|85|100 `; function setStatus(msg, isWarn = true) { statusPill.style.display = msg ? "inline-block" : "none"; statusPill.textContent = msg || ""; if (isWarn) statusPill.classList.add("pill-warn"); } function normalizeLine(line) { return (line ?? "").replace(/\r/g, "").trimEnd(); } function splitSlides(raw) { // Supports multiple separator occurrences const parts = raw.split(/^---\s*slide\s*---\s*$/m); // First split may contain text before first separator -> ignore empty return parts .map(p => p.trim()) .filter(p => p.length > 0); } function parseCsvLike(str) { // Parses "A,B,C" into ["A","B","C"], trimming spaces. if (!str) return []; return str.split(",").map(s => s.trim()).filter(Boolean); } function parseTableRowsSpec(rowsSpec) { // rowsSpec format: "r1c1|r1c2|...; r2c1|r2c2|..." // returns array of array cells if (!rowsSpec) return []; return rowsSpec.split(";").map(r => r.trim()).filter(Boolean).map(row => { return row.split("|").map(c => c.trim()); }); } function parseSlideBlock(block) { // Simple key parsing: // title = ... // subtitle = ... // bullets: // - ... // chart: key=val; key=val // table: columns=...; rows=...; (rows are r1|r2|... separated by ;) const lines = block.split("\n").map(normalizeLine); const s = { title: "Título", subtitle: "", bullets: [], chart: null, // {type, title, labels:[], series:[...]} table: null // {columns:[], rows:[[]]} }; let i = 0; while (i < lines.length) { const line = lines[i].trim(); if (!line) { i++; continue; } if (line.startsWith("title")) { const m = line.match(/^\s*title\s*=\s*(.*)\s*$/); if (m) s.title = m[1].trim(); i++; continue; } if (line.startsWith("subtitle")) { const m = line.match(/^\s*subtitle\s*=\s*(.*)\s*$/); if (m) s.subtitle = m[1].trim(); i++; continue; } if (line === "bullets:" || line.startsWith("bullets:")) { // collect subsequent lines starting with '-' i++; const bullets = []; while (i < lines.length) { const l2 = (lines[i] || "").trim(); if (!l2) { i++; continue; } if (l2.startsWith("- ")) { bullets.push(l2.slice(2).trim()); i++; continue; } // stop if we encounter a new key line if (/^(chart:|table:|title\s*=|subtitle\s*=|bullets:)/i.test(l2)) break; // otherwise ignore i++; } s.bullets = bullets; continue; } if (line.toLowerCase().startsWith("chart:")) { const spec = line.replace(/^chart:\s*/i, ""); // parse "type=bar; title=...; labels=A,B; series=1,2,3" const parts = spec.split(";").map(p => p.trim()).filter(Boolean); const chart = { type: "bar", title: "", labels: [], series: [] }; for (const p of parts) { const m = p.match(/^(\w+)\s*=\s*(.*)$/); if (!m) continue; const key = m[1].toLowerCase(); const val = m[2].trim(); if (key === "type") chart.type = val; else if (key === "title") chart.title = val; else if (key === "labels") chart.labels = parseCsvLike(val); else if (key === "series") chart.series = parseCsvLike(val).map(NumberOrString); } // if user gives series values but no labels, attempt to infer length from series if (chart.labels.length === 0 && chart.series.length) { chart.labels = chart.series.map((_, idx) => String(idx + 1)); } // ensure numeric for Chart.js chart.series = chart.series.map(v => typeof v === "number" ? v : Number(v)).map(v => isNaN(v) ? 0 : v); s.chart = chart; i++; continue; } if (line.toLowerCase().startsWith("table:")) { const spec = line.replace(/^table:\s*/i, ""); // "columns=...; rows=... " const parts = spec.split(";").map(p => p.trim()).filter(Boolean); const table = { columns: [], rows: [] }; for (const p of parts) { const m = p.match(/^(\w+)\s*=\s*(.*)$/); if (!m) continue; const key = m[1].toLowerCase(); const val = m[2].trim(); if (key === "columns") table.columns = parseCsvLike(val); else if (key === "rows") table.rows = parseTableRowsSpec(val); } // Normalize row lengths if (table.columns.length) { table.rows = (table.rows || []).map(r => { const rr = r.slice(0, table.columns.length); while (rr.length < table.columns.length) rr.push(""); return rr; }); } s.table = table; i++; continue; } // Unknown line: ignore i++; } // Normalize bullets if empty if (!Array.isArray(s.bullets)) s.bullets = []; return s; } function NumberOrString(v) { const n = Number(v); return isNaN(n) ? v : n; } function parseAllSlides(raw) { const blocks = splitSlides(raw); const parsed = blocks.map(parseSlideBlock).filter(Boolean); if (parsed.length === 0) parsed.push({ title: "Sin contenido", subtitle:"", bullets:[], chart:null, table:null }); return parsed; } // Render let chartInstance = null; let currentChartCanvas = null; function escapeHtml(s) { return String(s ?? "") .replaceAll("&","&") .replaceAll("<","<") .replaceAll(">",">") .replaceAll('"',""") .replaceAll("'","'"); } function renderSlide() { const s = slides[selectedIndex]; pptSlide.innerHTML = ""; pptSlide.classList.remove("slide-has-bullets","slide-has-chart","slide-has-table"); if (s.bullets && s.bullets.length) pptSlide.classList.add("slide-has-bullets"); if (s.chart) pptSlide.classList.add("slide-has-chart"); if (s.table && s.table.columns && s.table.columns.length) pptSlide.classList.add("slide-has-table"); // Title const titleEl = document.createElement("div"); titleEl.className = "slide-title"; titleEl.textContent = s.title || ""; pptSlide.appendChild(titleEl); // Subtitle if (s.subtitle) { const subEl = document.createElement("div"); subEl.className = "slide-subtitle"; subEl.textContent = s.subtitle; pptSlide.appendChild(subEl); } // Body bullets if (s.bullets && s.bullets.length) { const bodyEl = document.createElement("div"); bodyEl.className = "slide-body"; const ul = document.createElement("ul"); for (const b of s.bullets) { const li = document.createElement("li"); li.textContent = b; ul.appendChild(li); } bodyEl.appendChild(ul); pptSlide.appendChild(bodyEl); } // Chart if (s.chart) { const chartEl = document.createElement("div"); chartEl.className = "slide-chart"; const canvas = document.createElement("canvas"); canvas.id = `chartCanvas_${selectedIndex}_${Date.now()}`; chartEl.appendChild(canvas); pptSlide.appendChild(chartEl); // Render Chart.js // Cleanup old instance if (chartInstance) chartInstance.destroy(); currentChartCanvas = canvas; const ctx = canvas.getContext("2d"); const type = String(s.chart.type || "bar").toLowerCase(); const chartTypeMap = { "bar": "bar", "line": "line", "pie": "pie" }; const chartType = chartTypeMap[type] || "bar"; const labels = s.chart.labels || []; const values = (s.chart.series || []).map(v => Number(v) || 0); chartInstance = new Chart(ctx, { type: chartType, data: { labels, datasets: [{ label: s.chart.title || "Serie", data: values, borderWidth: 2, backgroundColor: chartType === "pie" ? undefined : "rgba(109,94,252,0.45)", borderColor: "rgba(109,94,252,0.95)", pointBackgroundColor: "rgba(45,212,191,0.95)", pointBorderColor: "rgba(45,212,191,0.95)" }] }, options: { responsive: false, maintainAspectRatio: false, plugins: { legend: { display: chartType !== "pie" }, title: { display: Boolean(s.chart.title), text: s.chart.title, color: "#0b1220", font: { size: 14, weight: "800" } }, tooltip: { enabled: true } }, scales: chartType === "pie" ? {} : { x: { ticks: { color: "#0b1220" }, grid: { color: "rgba(0,0,0,.06)" } }, y: { ticks: { color: "#0b1220" }, grid: { color: "rgba(0,0,0,.06)" } } } } }); } // Table if (s.table && s.table.columns && s.table.columns.length) { const tableWrap = document.createElement("div"); tableWrap.className = "slide-table"; const table = document.createElement("table"); const thead = document.createElement("thead"); const trh = document.createElement("tr"); for (const c of s.table.columns) { const th = document.createElement("th"); th.textContent = c; trh.appendChild(th); } thead.appendChild(trh); const tbody = document.createElement("tbody"); const rows = (s.table.rows || []); for (const row of rows) { const tr = document.createElement("tr"); for (let k = 0; k < s.table.columns.length; k++) { const td = document.createElement("td"); td.textContent = row[k] ?? ""; tr.appendChild(td); } tbody.appendChild(tr); } table.appendChild(thead); table.appendChild(tbody); tableWrap.appendChild(table); pptSlide.appendChild(tableWrap); } updateEditor(); } function renderThumbs() { thumbs.innerHTML = ""; slides.forEach((s, idx) => { const t = document.createElement("div"); t.className = "thumb" + (idx === selectedIndex ? " active" : ""); t.title = `${idx + 1}: ${s.title || ""}`; t.textContent = idx + 1; t.addEventListener("click", () => { selectedIndex = idx; syncSelection(); }); thumbs.appendChild(t); }); } function syncSelection() { slideIndexLabel.textContent = String(selectedIndex + 1); slideCountLabel.textContent = String(slides.length); renderSlide(); renderThumbs(); } // Editor UI function updateEditor() { const s = slides[selectedIndex]; editor.innerHTML = `
Texto
Slide ${selectedIndex + 1}
Título
Subtítulo (opcional)
Bullets (1 por línea)
Gráfico
${renderChartEditorInner(s.chart)}
Tabla
${renderTableEditorInner(s.table)}
`; // Bind events $("#edTitle").addEventListener("input", (e) => { s.title = e.target.value; renderSlide(); }); $("#edSubtitle").addEventListener("input", (e) => { s.subtitle = e.target.value; renderSlide(); }); $("#edBullets").addEventListener("input", (e) => { const raw = e.target.value; s.bullets = raw.split("\n").map(x => x.trim()).filter(Boolean); renderSlide(); }); // Toggle chart $("#btnToggleChart").addEventListener("click", () => { if (slides[selectedIndex].chart) { slides[selectedIndex].chart = null; } else { slides[selectedIndex].chart = { type: "bar", title: "Mi gráfico", labels: ["A","B","C"], series: [1,2,3] }; } updateEditorOnly(); renderSlide(); }); // chart editor bindings bindChartEditorEvents(); // Table add/remove $("#btnAddTable").addEventListener("click", () => { if (slides[selectedIndex].table) { slides[selectedIndex].table = null; } else { slides[selectedIndex].table = { columns: ["Col1","Col2","Col3"], rows: [ ["1","2","3"], ["4","5","6"] ] }; } updateEditorOnly(); renderSlide(); }); bindTableEditorEvents(); } function updateEditorOnly() { updateEditor(); // simplest for MVP } function renderChartEditorInner(chart) { if (!chart) { return `
No hay gráfico en este slide. Usa Agregar arriba.
`; } const type = chart.type || "bar"; const labelsCsv = (chart.labels || []).join(","); const seriesCsv = (chart.series || []).join(","); return `
Tipo
Título del gráfico
Labels (CSV)
Valores (CSV)
`; } function bindChartEditorEvents() { const s = slides[selectedIndex]; if (!s.chart) return; const edChartType = $("#edChartType"); if (edChartType) edChartType.addEventListener("change", (e) => { s.chart.type = e.target.value; renderSlide(); }); const edChartTitle = $("#edChartTitle"); if (edChartTitle) edChartTitle.addEventListener("input", (e) => { s.chart.title = e.target.value; renderSlide(); }); const edChartLabels = $("#edChartLabels"); if (edChartLabels) edChartLabels.addEventListener("input", (e) => { const labels = e.target.value.split(",").map(x => x.trim()).filter(Boolean); s.chart.labels = labels; renderSlide(); }); const edChartSeries = $("#edChartSeries"); if (edChartSeries) edChartSeries.addEventListener("input", (e) => { const vals = e.target.value.split(",").map(x => x.trim()).filter(Boolean).map(v => Number(v)); s.chart.series = vals.map(v => isNaN(v) ? 0 : v); renderSlide(); }); } function renderTableEditorInner(table) { if (!table) { return `
No hay tabla en este slide. Usa Agregar arriba.
`; } const cols = table.columns || []; const rows = table.rows || []; const colsCsv = cols.join(","); // Make grid editable const ths = cols.map(c => `${escapeHtml(c)}`).join(""); const trs = rows.map((r, ri) => { const tds = cols.map((_, ci) => { const val = (r && r[ci] != null) ? r[ci] : ""; return ``; }).join(""); return `${tds}`; }).join(""); return `
Columnas (CSV)
${ths} ${trs || ``}
Sin filas
`; } function bindTableEditorEvents() { const s = slides[selectedIndex]; if (!s.table) return; const edTableColumns = $("#edTableColumns"); if (edTableColumns) { edTableColumns.addEventListener("input", (e) => { const cols = e.target.value.split(",").map(x => x.trim()).filter(Boolean); if (!cols.length) return; // rebuild rows to match new cols length const oldRows = s.table.rows || []; s.table.columns = cols; s.table.rows = oldRows.map(r => { const rr = (r || []).slice(0, cols.length); while (rr.length < cols.length) rr.push(""); return rr; }); renderSlide(); updateEditorOnly(); }); } // Cell inputs (event delegation) editor.querySelectorAll(".cell-input").forEach(inp => { inp.addEventListener("input", (e) => { const r = Number(inp.dataset.r); const c = Number(inp.dataset.c); if (!s.table.rows[r]) s.table.rows[r] = []; s.table.rows[r][c] = inp.value; renderSlide(); // update preview + later export }); }); const btnTableAddRow = $("#btnTableAddRow"); if (btnTableAddRow) btnTableAddRow.addEventListener("click", () => { const colsLen = (s.table.columns || []).length; s.table.rows.push(Array.from({ length: colsLen }, () => "")); updateEditorOnly(); renderSlide(); }); const btnTableAddCol = $("#btnTableAddCol"); if (btnTableAddCol) btnTableAddCol.addEventListener("click", () => { const cols = s.table.columns || []; const newColName = `Col${cols.length + 1}`; cols.push(newColName); const rows = s.table.rows || []; s.table.rows = rows.map(r => { r = r || []; r.push(""); return r; }); updateEditorOnly(); renderSlide(); }); const btnTableRemoveLastRow = $("#btnTableRemoveLastRow"); if (btnTableRemoveLastRow) btnTableRemoveLastRow.addEventListener("click", () => { if ((s.table.rows || []).length <= 1) return; s.table.rows.pop(); updateEditorOnly(); renderSlide(); }); } // Throttled parsing & render btnParse.addEventListener("click", () => { try { const raw = inputSlides.value || ""; slides = parseAllSlides(raw); selectedIndex = Math.min(selectedIndex, slides.length - 1); setStatus("", false); syncSelection(); } catch (err) { console.error(err); setStatus("Error al parsear: revisa la sintaxis del input.", true); alert("Error: " + (err?.message || err)); } }); btnLoadExample.addEventListener("click", () => { inputSlides.value = EXAMPLE; slides = parseAllSlides(EXAMPLE); selectedIndex = 0; syncSelection(); }); btnPrev.addEventListener("click", () => { if (!slides.length) return; selectedIndex = (selectedIndex - 1 + slides.length) % slides.length; syncSelection(); }); btnNext.addEventListener("click", () => { if (!slides.length) return; selectedIndex = (selectedIndex + 1) % slides.length; syncSelection(); }); // PPTX export (PptxGenJS + capturas desde el DOM) async function captureChartAsImageDataURL() { if (!slides[selectedIndex].chart) return null; // Ensure we have a chart canvas const canvas = currentChartCanvas || document.querySelector("canvas"); if (!canvas) return null; // Give chart a moment to be fully rendered await sleep(120); // Convert canvas to PNG data URL return canvas.toDataURL("image/png"); } async function captureTableAsImageDataURL() { if (!slides[selectedIndex].table) return null; const tableEl = pptSlide.querySelector(".slide-table"); if (!tableEl) return null; // Render table to canvas await sleep(50); const canvas = await html2canvas(tableEl, { backgroundColor: "#ffffff", scale: 2, useCORS: true }); return canvas.toDataURL("image/png"); } function pptMeasureFromPx(px, scale) { // Not used directly; we mostly use image placement in inches. return px * (1 / 96) * scale; } async function exportSlidesToPptx() { btnExportPptx.disabled = true; btnExportPptx.textContent = "Generando..."; try { const pptx = new PptxGenJS(); // default layout matches 10 x 7.5 inches pptx.layout = "LAYOUT_WIDE"; // Styles const themeColor = "4F46E5"; // indigo-ish const textColor = "0B1220"; // We'll render each slide: we need chart/table images. // Since chart & table are drawn in the DOM, we will iterate: // - select slide i // - render // - capture chart/table as images (if present) // - add slide to pptx for (let i = 0; i < slides.length; i++) { selectedIndex = i; syncSelection(); await sleep(160); const s = slides[i]; // base slide const slide = pptx.addSlide(); // Title slide.addText(s.title || "", { x: 0.5, y: 0.35, w: 9.0, h: 0.6, fontSize: 22, bold: true, color: textColor, valign: "top" }); // Subtitle (optional) if (s.subtitle) { slide.addText(s.subtitle, { x: 0.5, y: 0.95, w: 9.0, h: 0.3, fontSize: 12, color: "334155", valign: "top" }); } // Bullets: left column if (s.bullets && s.bullets.length) { // Add bullets as a single text block with line breaks. // PPTX bullet style support is limited; we simulate with hyphen lines. const bulletText = s.bullets.map(b => `• ${b}`).join("\n"); slide.addText(bulletText, { x: 0.7, y: 1.25, w: 4.7, h: 3.9, fontSize: 14, color: textColor, valign: "top", lineSpacingMultiple: 1.05 }); } // Chart image (right/top-ish) let chartDataUrl = null; let tableDataUrl = null; if (s.chart) chartDataUrl = await captureChartAsImageDataURL(); if (s.table) tableDataUrl = await captureTableAsImageDataURL(); // Place chart if (chartDataUrl) { // x,y,w,h in inches slide.addImage({ data: chartDataUrl, x: 6.0, y: 1.55, w: 3.7, h: 2.9 }); } // Place table if (tableDataUrl) { slide.addImage({ data: tableDataUrl, x: 0.45, y: 4.35, w: 9.4, h: 3.0 }); } // Footer small label slide.addText(`Generado con Gamma-like`, { x: 0.5, y: 7.1, w: 2.5, h: 0.25, fontSize: 10, color: "64748b" }); } const filename = `presentacion-gamma-like-${Date.now()}.pptx`; await pptx.writeFile({ fileName: filename }); } finally { btnExportPptx.disabled = false; btnExportPptx.textContent = "Descargar .pptx"; } } btnExportPptx.addEventListener("click", async () => { if (!slides.length) { alert("Primero genera la vista previa (Parse)."); return; } // Basic warning: export uses screenshots (chart/table) const ok = confirm("La exportación .pptx tomará capturas (imágenes) de gráficos y tablas desde la vista previa. ¿Continuar?"); if (!ok) return; setStatus("Exportando PPTX... (esto puede tardar)", false); await exportSlidesToPptx(); setStatus("Exportación lista ✅", false); setTimeout(() => setStatus("", false), 2000); }); function sleep(ms) { return new Promise(res => setTimeout(res, ms)); } // Initialize inputSlides.value = EXAMPLE; slides = parseAllSlides(EXAMPLE); selectedIndex = 0; syncSelection(); })();