CoolFace
Apppublic

KEXEL/git

sourceHugging Faceupdated 1mo agoView on Hugging Face
0likes
slideforge.html1546 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="pt-BR">3<head>4<meta charset="UTF-8">5<meta name="viewport" content="width=device-width, initial-scale=1.0">6<title>SlideForge - Editor PPTX</title>7<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">8<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">9<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>10<style>11:root {12  --bg: #0c0e14;13  --surface: #13161f;14  --card: #1a1e2b;15  --card-hover: #222738;16  --border: #2a2f42;17  --border-light: #353b52;18  --text: #e4e7f0;19  --text-dim: #a0a6c0;20  --muted: #6b7394;21  --accent: #10b981;22  --accent-hover: #34d399;23  --accent-dim: rgba(16,185,129,0.15);24  --danger: #ef4444;25  --danger-dim: rgba(239,68,68,0.15);26  --warning: #f59e0b;27  --selection: rgba(16,185,129,0.4);28}29*{margin:0;padding:0;box-sizing:border-box;}30body{font-family:'Space Grotesk',sans-serif;background:var(--bg);color:var(--text);overflow:hidden;height:100vh;display:flex;flex-direction:column;}31::-webkit-scrollbar{width:6px;}32::-webkit-scrollbar-track{background:var(--surface);}33::-webkit-scrollbar-thumb{background:var(--border-light);border-radius:3px;}34::-webkit-scrollbar-thumb:hover{background:var(--muted);}35 36/* TOOLBAR */37#toolbar{display:flex;align-items:center;gap:4px;padding:6px 12px;background:var(--surface);border-bottom:1px solid var(--border);min-height:48px;flex-shrink:0;overflow-x:auto;}38.tool-group{display:flex;align-items:center;gap:3px;}39.tool-sep{width:1px;height:28px;background:var(--border);margin:0 6px;flex-shrink:0;}40#toolbar button{background:transparent;border:1px solid transparent;color:var(--text-dim);width:34px;height:34px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .15s;}41#toolbar button:hover{background:var(--card);color:var(--text);border-color:var(--border);}42#toolbar button.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent);}43#toolbar select,#toolbar input[type="number"]{background:var(--card);border:1px solid var(--border);color:var(--text);padding:4px 6px;border-radius:5px;font-size:12px;font-family:inherit;height:30px;outline:none;}44#toolbar select:focus,#toolbar input[type="number"]:focus{border-color:var(--accent);}45#toolbar input[type="color"]{width:30px;height:30px;border:1px solid var(--border);border-radius:5px;background:var(--card);cursor:pointer;padding:2px;}46#toolbar input[type="color"]::-webkit-color-swatch-wrapper{padding:0;}47#toolbar input[type="color"]::-webkit-color-swatch{border:none;border-radius:3px;}48#toolbar input[type="number"]{width:52px;text-align:center;}49 50/* MAIN LAYOUT */51#main{display:flex;flex:1;overflow:hidden;}52 53/* SLIDES PANEL */54#slides-panel{width:170px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;}55#slides-panel h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);padding:10px 12px 6px;font-weight:500;}56#slides-list{flex:1;overflow-y:auto;padding:0 8px 8px;display:flex;flex-direction:column;gap:6px;}57.slide-thumb{position:relative;cursor:pointer;border-radius:6px;border:2px solid transparent;overflow:hidden;aspect-ratio:4/3;background:#fff;transition:all .15s;flex-shrink:0;}58.slide-thumb:hover{border-color:var(--border-light);}59.slide-thumb.active{border-color:var(--accent);box-shadow:0 0 12px var(--accent-dim);}60.slide-thumb-num{position:absolute;bottom:2px;right:4px;font-size:9px;color:#999;background:rgba(255,255,255,0.85);padding:0 4px;border-radius:3px;font-weight:600;}61.slide-thumb-content{width:100%;height:100%;position:relative;overflow:hidden;transform-origin:top left;}62.thumb-el{position:absolute;overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:3px;line-height:1.1;color:#333;word-break:break-word;padding:1px;}63#btn-add-slide-panel{margin:4px 8px 8px;padding:8px;border-radius:6px;border:1px dashed var(--border);background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-family:inherit;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:6px;}64#btn-add-slide-panel:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim);}65 66/* EDITOR AREA */67#editor-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg);position:relative;overflow:hidden;}68#editor-area::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at center,rgba(16,185,129,0.03) 0%,transparent 70%);pointer-events:none;}69#slide-wrapper{position:relative;box-shadow:0 8px 40px rgba(0,0,0,0.5),0 0 0 1px rgba(255,255,255,0.05);}70#slide-canvas{position:relative;overflow:hidden;background:#fff;}71 72/* SLIDE ELEMENTS */73.slide-element{position:absolute;cursor:move;user-select:none;overflow:hidden;}74.slide-element.editing{cursor:text;user-select:text;outline:2px solid var(--accent);outline-offset:-1px;}75.slide-element.selected{outline:2px solid var(--accent);outline-offset:-1px;}76.slide-element.selected::after{content:'';position:absolute;inset:-4px;border:1px dashed var(--accent);pointer-events:none;}77.resize-handle{position:absolute;width:8px;height:8px;background:var(--accent);border:1px solid #fff;border-radius:2px;z-index:10;display:none;}78.slide-element.selected .resize-handle{display:block;}79.resize-handle.nw{top:-4px;left:-4px;cursor:nw-resize;}80.resize-handle.ne{top:-4px;right:-4px;cursor:ne-resize;}81.resize-handle.sw{bottom:-4px;left:-4px;cursor:sw-resize;}82.resize-handle.se{bottom:-4px;right:-4px;cursor:se-resize;}83.resize-handle.n{top:-4px;left:50%;transform:translateX(-50%);cursor:n-resize;}84.resize-handle.s{bottom:-4px;left:50%;transform:translateX(-50%);cursor:s-resize;}85.resize-handle.w{top:50%;left:-4px;transform:translateY(-50%);cursor:w-resize;}86.resize-handle.e{top:50%;right:-4px;transform:translateY(-50%);cursor:e-resize;}87 88/* PROPERTIES PANEL */89#props-panel{width:220px;background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;overflow-y:auto;}90#props-panel h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);padding:10px 12px 6px;font-weight:500;}91.prop-section{padding:6px 12px 12px;}92.prop-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;}93.prop-label{font-size:11px;color:var(--muted);width:24px;text-align:right;flex-shrink:0;}94.prop-input{flex:1;background:var(--card);border:1px solid var(--border);color:var(--text);padding:4px 6px;border-radius:4px;font-size:11px;font-family:inherit;outline:none;height:28px;}95.prop-input:focus{border-color:var(--accent);}96.prop-color{width:28px;height:28px;border:1px solid var(--border);border-radius:4px;background:var(--card);cursor:pointer;padding:1px;}97.prop-color::-webkit-color-swatch-wrapper{padding:0;}98.prop-color::-webkit-color-swatch{border:none;border-radius:2px;}99.prop-btn{padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card);color:var(--text-dim);cursor:pointer;font-size:11px;font-family:inherit;transition:all .12s;}100.prop-btn:hover{background:var(--card-hover);color:var(--text);}101.prop-btn.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent);}102.prop-btn.danger:hover{background:var(--danger-dim);color:var(--danger);border-color:var(--danger);}103.no-selection{color:var(--muted);font-size:12px;text-align:center;padding:20px 12px;line-height:1.6;}104 105/* EMPTY STATE */106#empty-state{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:20;background:var(--bg);}107#empty-state.hidden{display:none;}108.empty-icon{font-size:64px;color:var(--border-light);margin-bottom:20px;}109.empty-title{font-size:28px;font-weight:700;margin-bottom:8px;background:linear-gradient(135deg,var(--accent),#06b6d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}110.empty-sub{color:var(--muted);font-size:14px;margin-bottom:28px;}111.empty-actions{display:flex;gap:12px;}112.empty-btn{padding:12px 28px;border-radius:8px;font-size:14px;font-weight:600;font-family:inherit;cursor:pointer;transition:all .2s;border:none;display:flex;align-items:center;gap:8px;}113.empty-btn.primary{background:var(--accent);color:#fff;}114.empty-btn.primary:hover{background:var(--accent-hover);transform:translateY(-1px);box-shadow:0 4px 20px rgba(16,185,129,0.3);}115.empty-btn.secondary{background:var(--card);color:var(--text);border:1px solid var(--border);}116.empty-btn.secondary:hover{background:var(--card-hover);border-color:var(--border-light);}117 118/* TOAST */119#toast-container{position:fixed;top:60px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;}120.toast{padding:10px 16px;border-radius:8px;font-size:13px;font-family:inherit;color:#fff;animation:toastIn .3s ease;box-shadow:0 4px 20px rgba(0,0,0,0.4);display:flex;align-items:center;gap:8px;max-width:320px;}121.toast.success{background:#065f46;border:1px solid var(--accent);}122.toast.error{background:#7f1d1d;border:1px solid var(--danger);}123.toast.info{background:#1e3a5f;border:1px solid #3b82f6;}124.toast.fadeout{animation:toastOut .3s ease forwards;}125@keyframes toastIn{from{opacity:0;transform:translateX(40px);}to{opacity:1;transform:translateX(0);}}126@keyframes toastOut{from{opacity:1;transform:translateX(0);}to{opacity:0;transform:translateX(40px);}}127 128/* FILE INPUT HIDDEN */129#file-input{display:none;}130 131/* STATUS BAR */132#status-bar{height:26px;background:var(--surface);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 12px;font-size:11px;color:var(--muted);gap:16px;flex-shrink:0;}133#status-bar span{display:flex;align-items:center;gap:4px;}134</style>135</head>136<body>137 138<!-- BARRA DE FERRAMENTAS -->139<div id="toolbar">140  <div class="tool-group">141    <button id="btn-new" title="Nova Apresentação (Ctrl+N)"><i class="fas fa-file"></i></button>142    <button id="btn-open" title="Abrir Arquivo (Ctrl+O)"><i class="fas fa-folder-open"></i></button>143    <button id="btn-save" title="Salvar PPTX (Ctrl+S)"><i class="fas fa-download"></i></button>144  </div>145  <div class="tool-sep"></div>146  <div class="tool-group">147    <button id="btn-undo" title="Desfazer (Ctrl+Z)"><i class="fas fa-undo"></i></button>148    <button id="btn-redo" title="Refazer (Ctrl+Y)"><i class="fas fa-redo"></i></button>149  </div>150  <div class="tool-sep"></div>151  <div class="tool-group" id="text-tools">152    <select id="font-select" title="Fonte">153      <option value="Calibri">Calibri</option>154      <option value="Arial">Arial</option>155      <option value="Times New Roman">Times New Roman</option>156      <option value="Courier New">Courier New</option>157      <option value="Georgia">Georgia</option>158      <option value="Verdana">Verdana</option>159      <option value="Tahoma">Tahoma</option>160      <option value="Impact">Impact</option>161      <option value="Comic Sans MS">Comic Sans MS</option>162    </select>163    <input type="number" id="font-size" min="6" max="200" value="18" title="Tamanho (pt)">164    <button id="btn-bold" title="Negrito (Ctrl+B)"><i class="fas fa-bold"></i></button>165    <button id="btn-italic" title="Itálico (Ctrl+I)"><i class="fas fa-italic"></i></button>166    <button id="btn-underline" title="Sublinhado (Ctrl+U)"><i class="fas fa-underline"></i></button>167    <input type="color" id="font-color" value="#000000" title="Cor do Texto">168    <button id="btn-align-l" title="Alinhar à Esquerda"><i class="fas fa-align-left"></i></button>169    <button id="btn-align-c" title="Centralizar"><i class="fas fa-align-center"></i></button>170    <button id="btn-align-r" title="Alinhar à Direita"><i class="fas fa-align-right"></i></button>171  </div>172  <div class="tool-sep"></div>173  <div class="tool-group">174    <button id="btn-add-text" title="Caixa de Texto"><i class="fas fa-font"></i></button>175    <button id="btn-add-rect" title="Retângulo"><i class="far fa-square"></i></button>176    <button id="btn-add-oval" title="Oval"><i class="far fa-circle"></i></button>177  </div>178  <div class="tool-sep"></div>179  <div class="tool-group">180    <button id="btn-add-slide" title="Novo Slide"><i class="fas fa-plus"></i></button>181    <button id="btn-dup-slide" title="Duplicar Slide"><i class="fas fa-copy"></i></button>182    <button id="btn-del-slide" title="Excluir Slide"><i class="fas fa-trash"></i></button>183  </div>184</div>185 186<!-- CONTEÚDO PRINCIPAL -->187<div id="main">188  <!-- PAINEL DE SLIDES -->189  <div id="slides-panel">190    <h4>Slides</h4>191    <div id="slides-list"></div>192    <button id="btn-add-slide-panel"><i class="fas fa-plus"></i> Novo Slide</button>193  </div>194 195  <!-- ÁREA DO EDITOR -->196  <div id="editor-area">197    <div id="empty-state">198      <div class="empty-icon"><i class="fas fa-file-powerpoint"></i></div>199      <div class="empty-title">SlideForge</div>200      <div class="empty-sub">Editor de apresentações PPTX no navegador</div>201      <div class="empty-actions">202        <button class="empty-btn primary" id="btn-new-empty"><i class="fas fa-plus"></i> Nova Apresentação</button>203        <button class="empty-btn secondary" id="btn-open-empty"><i class="fas fa-folder-open"></i> Abrir Arquivo</button>204      </div>205    </div>206    <div id="slide-wrapper" style="display:none;">207      <div id="slide-canvas"></div>208    </div>209  </div>210 211  <!-- PAINEL DE PROPRIEDADES -->212  <div id="props-panel">213    <h4>Propriedades</h4>214    <div id="props-content">215      <p class="no-selection">Selecione um elemento no slide para ver suas propriedades</p>216    </div>217  </div>218</div>219 220<!-- BARRA DE STATUS -->221<div id="status-bar">222  <span id="status-file"><i class="fas fa-file"></i> Nenhum arquivo</span>223  <span id="status-slide"><i class="fas fa-layer-group"></i> Slide -</span>224  <span id="status-elems"><i class="fas fa-shapes"></i> 0 elementos</span>225  <span style="margin-left:auto;" id="status-zoom"><i class="fas fa-search"></i> 100%</span>226</div>227 228<!-- TOAST CONTAINER -->229<div id="toast-container"></div>230 231<!-- INPUT DE ARQUIVO OCULTO -->232<input type="file" id="file-input" accept=".pptx">233 234<script>235// ============================================================236// ESTADO GLOBAL237// ============================================================238const NS_P = 'http://schemas.openxmlformats.org/presentationml/2006/main';239const NS_A = 'http://schemas.openxmlformats.org/drawingml/2006/main';240const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';241 242const state = {243  zip: null,244  fileName: 'Apresentacao',245  slides: [],246  currentSlide: 0,247  selectedId: null,248  editing: false,249  dragging: false,250  resizing: false,251  resizeDir: '',252  dragStart: { x: 0, y: 0 },253  elemStart: { x: 0, y: 0, w: 0, h: 0 },254  scale: 1,255  nextId: 200,256  history: [],257  historyIdx: -1258};259 260// ============================================================261// UTILITÁRIOS262// ============================================================263function escXml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }264function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }265 266function toast(msg, type='info') {267  const c = document.getElementById('toast-container');268  const t = document.createElement('div');269  t.className = 'toast ' + type;270  const icons = { success:'fa-check-circle', error:'fa-exclamation-circle', info:'fa-info-circle' };271  t.innerHTML = `<i class="fas ${icons[type]||icons.info}"></i> ${escHtml(msg)}`;272  c.appendChild(t);273  setTimeout(() => { t.classList.add('fadeout'); setTimeout(() => t.remove(), 300); }, 3000);274}275 276function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }277 278function pushHistory() {279  state.history = state.history.slice(0, state.historyIdx + 1);280  state.history.push(deepClone(state.slides));281  state.historyIdx = state.history.length - 1;282  if (state.history.length > 50) { state.history.shift(); state.historyIdx--; }283}284 285function undo() {286  if (state.historyIdx > 0) {287    state.historyIdx--;288    state.slides = deepClone(state.history[state.historyIdx]);289    state.selectedId = null;290    renderAll();291    toast('Desfeito', 'info');292  }293}294 295function redo() {296  if (state.historyIdx < state.history.length - 1) {297    state.historyIdx++;298    state.slides = deepClone(state.history[state.historyIdx]);299    state.selectedId = null;300    renderAll();301    toast('Refeito', 'info');302  }303}304 305// ============================================================306// PARSER PPTX307// ============================================================308function qns(parent, local, ns) {309  return parent.getElementsByTagNameNS(ns, local);310}311 312function getAttr(node, local, ns, attr) {313  const els = node.getElementsByTagNameNS(ns, local);314  return els.length > 0 ? els[0].getAttribute(attr) : null;315}316 317async function parsePPTX(zip) {318  const slides = [];319 320  // Encontrar todos os arquivos de slide321  const slideFiles = [];322  zip.forEach((path) => {323    const m = path.match(/^ppt\/slides\/slide(\d+)\.xml$/);324    if (m) slideFiles.push({ path, num: parseInt(m[1]) });325  });326  slideFiles.sort((a, b) => a.num - b.num);327 328  // Ler relações do presentation.xml.rels para mapear rIds329  let presRels = {};330  try {331    const relsXml = await zip.file('ppt/_rels/presentation.xml.rels').async('string');332    const relsDoc = new DOMParser().parseFromString(relsXml, 'application/xml');333    const rels = relsDoc.getElementsByTagName('Relationship');334    for (const r of rels) {335      presRels[r.getAttribute('Id')] = r.getAttribute('Target');336    }337  } catch(e) {}338 339  // Ler relações de cada slide340  for (const sf of slideFiles) {341    const slideXml = await zip.file(sf.path).async('string');342    const doc = new DOMParser().parseFromString(slideXml, 'application/xml');343    const slide = {344      xmlPath: sf.path,345      slideNum: sf.num,346      originalXml: slideXml,347      modified: false,348      width: 9144000,349      height: 6858000,350      background: '#FFFFFF',351      elements: []352    };353 354    // Dimensões do slide (do presentation.xml)355    // Usaremos padrão por enquanto356 357    // Relações deste slide358    let slideRels = {};359    try {360      const relPath = `ppt/slides/_rels/slide${sf.num}.xml.rels`;361      if (zip.file(relPath)) {362        const srXml = await zip.file(relPath).async('string');363        const srDoc = new DOMParser().parseFromString(srXml, 'application/xml');364        for (const r of srDoc.getElementsByTagName('Relationship')) {365          slideRels[r.getAttribute('Id')] = r.getAttribute('Target');366        }367      }368    } catch(e) {}369 370    // Background371    const bgEls = qns(doc, 'bg', NS_P);372    if (bgEls.length > 0) {373      const fills = qns(bgEls[0], 'solidFill', NS_A);374      if (fills.length > 0) {375        const clr = fills[0].firstElementChild;376        if (clr) {377          if (clr.localName === 'srgbClr') slide.background = '#' + clr.getAttribute('val');378          else if (clr.localName === 'schemeClr') {379            const map = { 'bg1':'#FFFFFF', 'tx1':'#000000', 'bg2':'#E7E6E6', 'tx2':'#44546A' };380            slide.background = map[clr.getAttribute('val')] || '#FFFFFF';381          }382        }383      }384    }385 386    // Elementos (shapes, pictures)387    const spTree = qns(doc, 'spTree', NS_P);388    if (spTree.length > 0) {389      // Shapes com texto (p:sp)390      const shapes = qns(spTree[0], 'sp', NS_P);391      for (const sp of shapes) {392        const elem = parseShape(sp);393        if (elem) slide.elements.push(elem);394      }395 396      // Imagens (p:pic)397      const pics = qns(spTree[0], 'pic', NS_P);398      for (const pic of pics) {399        const elem = await parsePicture(pic, slideRels, zip);400        if (elem) slide.elements.push(elem);401      }402    }403 404    slides.push(slide);405  }406 407  return slides;408}409 410function parseShape(sp) {411  // ID e nome412  const cNvPr = qns(sp, 'cNvPr', NS_P);413  if (cNvPr.length === 0) return null;414  const id = parseInt(cNvPr[0].getAttribute('id')) || state.nextId++;415  const name = cNvPr[0].getAttribute('name') || 'Shape';416 417  // É caixa de texto?418  const cNvSpPr = qns(sp, 'cNvSpPr', NS_P);419  const isTextBox = cNvSpPr.length > 0 && cNvSpPr[0].getAttribute('txBox') === '1';420 421  // Posição e tamanho422  const xfrm = qns(sp, 'xfrm', NS_A);423  let x = 457200, y = 457200, w = 914400, h = 457200;424  if (xfrm.length > 0) {425    const off = qns(xfrm[0], 'off', NS_A);426    const ext = qns(xfrm[0], 'ext', NS_A);427    if (off.length > 0) { x = parseInt(off[0].getAttribute('x')) || 0; y = parseInt(off[0].getAttribute('y')) || 0; }428    if (ext.length > 0) { w = parseInt(ext[0].getAttribute('cx')) || 914400; h = parseInt(ext[0].getAttribute('cy')) || 457200; }429  }430 431  // Tipo de forma geométrica432  const prstGeom = qns(sp, 'prstGeom', NS_A);433  let shapeType = 'rect';434  if (prstGeom.length > 0) {435    const prst = prstGeom[0].getAttribute('prst');436    if (prst && prst.includes('ellip') || prst === 'oval') shapeType = 'oval';437  }438 439  // Fill440  let fill = null;441  const solidFill = qns(sp, 'solidFill', NS_A);442  if (solidFill.length > 0) {443    const clr = solidFill[solidFill.length - 1].firstElementChild;444    if (clr && clr.localName === 'srgbClr') fill = clr.getAttribute('val');445  }446 447  // Texto448  const txBody = qns(sp, 'txBody', NS_A);449  let paragraphs = [];450  if (txBody.length > 0) {451    paragraphs = parseParagraphs(txBody[0]);452  }453 454  // Se não tem texto e não é caixa de texto, pode ser uma forma pura455  if (paragraphs.length === 0 && !isTextBox) {456    return {457      id, name, type: 'shape', shapeType,458      x, y, width: w, height: h,459      fill: fill || '4472C4', line: '2F528F'460    };461  }462 463  // Se tem texto (mesmo que vazio), é um elemento de texto464  if (paragraphs.length === 0) {465    paragraphs = [{ align: 'left', runs: [{ text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' }] }];466  }467 468  return {469    id, name, type: 'text', isTextBox,470    x, y, width: w, height: h,471    paragraphs,472    fill: fill || null,473    shapeType: shapeType !== 'rect' ? shapeType : null474  };475}476 477function parseParagraphs(txBody) {478  const paras = qns(txBody, 'p', NS_A);479  const result = [];480  for (const p of paras) {481    const para = { align: 'left', runs: [] };482    const pPr = qns(p, 'pPr', NS_A);483    if (pPr.length > 0) {484      const algn = pPr[0].getAttribute('algn');485      if (algn === 'ctr') para.align = 'center';486      else if (algn === 'r') para.align = 'right';487      else if (algn === 'just') para.align = 'justify';488    }489    const runs = qns(p, 'r', NS_A);490    for (const r of runs) {491      const run = { text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' };492      const t = qns(r, 't', NS_A);493      if (t.length > 0) run.text = t[0].textContent || '';494      const rPr = qns(r, 'rPr', NS_A);495      if (rPr.length > 0) {496        const pr = rPr[0];497        run.bold = pr.getAttribute('b') === '1';498        run.italic = pr.getAttribute('i') === '1';499        run.underline = pr.getAttribute('u') === 'sng' || pr.getAttribute('u') === '1';500        const sz = pr.getAttribute('sz');501        if (sz) run.size = parseInt(sz) / 100;502        // Cor503        const srgbClr = qns(pr, 'srgbClr', NS_A);504        if (srgbClr.length > 0) run.color = srgbClr[0].getAttribute('val') || '000000';505        else {506          const schemeClr = qns(pr, 'schemeClr', NS_A);507          if (schemeClr.length > 0) {508            const v = schemeClr[0].getAttribute('val');509            const map = { 'tx1':'#000000', 'tx2':'#44546A', 'bg1':'#FFFFFF' };510            run.color = (map[v] || '#000000').replace('#', '');511          }512        }513        // Fonte514        const latin = pr.getAttribute('latin');515        if (latin) run.font = latin;516      }517      para.runs.push(run);518    }519    // Se não tem runs, adicionar um vazio520    if (para.runs.length === 0) {521      para.runs.push({ text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' });522    }523    result.push(para);524  }525  return result.length > 0 ? result : [{ align: 'left', runs: [{ text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' }] }];526}527 528async function parsePicture(pic, slideRels, zip) {529  const cNvPr = qns(pic, 'cNvPr', NS_P);530  if (cNvPr.length === 0) return null;531  const id = parseInt(cNvPr[0].getAttribute('id')) || state.nextId++;532  const name = cNvPr[0].getAttribute('name') || 'Imagem';533 534  const xfrm = qns(pic, 'xfrm', NS_A);535  let x = 0, y = 0, w = 914400, h = 685800;536  if (xfrm.length > 0) {537    const off = qns(xfrm[0], 'off', NS_A);538    const ext = qns(xfrm[0], 'ext', NS_A);539    if (off.length > 0) { x = parseInt(off[0].getAttribute('x')) || 0; y = parseInt(off[0].getAttribute('y')) || 0; }540    if (ext.length > 0) { w = parseInt(ext[0].getAttribute('cx')) || 914400; h = parseInt(ext[0].getAttribute('cy')) || 685800; }541  }542 543  // Imagem embutida544  const blip = qns(pic, 'blip', NS_A);545  let dataUrl = '';546  if (blip.length > 0) {547    const rEmbed = blip[0].getAttributeNS(NS_R, 'embed');548    if (rEmbed && slideRels[rEmbed]) {549      const imgPath = 'ppt/slides/' + slideRels[rEmbed].replace('../', '');550      const imgFile = zip.file(imgPath);551      if (imgFile) {552        const blob = await imgFile.async('blob');553        dataUrl = await new Promise(res => {554          const fr = new FileReader();555          fr.onload = () => res(fr.result);556          fr.readAsDataURL(blob);557        });558      }559    }560  }561 562  return { id, name, type: 'image', x, y, width: w, height: h, dataUrl, imgPath: slideRels[blip[0]?.getAttributeNS(NS_R, 'embed')] || '' };563}564 565// ============================================================566// ESCRITOR PPTX567// ============================================================568function buildSlideXml(slide) {569  let shapesXml = '';570  for (const el of slide.elements) {571    if (el.type === 'text') shapesXml += buildTextShapeXml(el);572    else if (el.type === 'shape') shapesXml += buildPureShapeXml(el);573    else if (el.type === 'image') shapesXml += buildPicXml(el);574  }575  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>576<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">577<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>${shapesXml}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;578}579 580function buildTextShapeXml(el) {581  const id = el.id;582  const name = escXml(el.name);583  const txBox = el.isTextBox ? '1' : '0';584  let parasXml = '';585  for (const p of el.paragraphs) {586    const algnMap = { left:'l', center:'ctr', right:'r', justify:'just' };587    const pPr = `<a:pPr algn="${algnMap[p.align] || 'l'}"/>`;588    let runsXml = '';589    for (const r of p.runs) {590      let rPrAttrs = `lang="pt-BR" sz="${Math.round(r.size * 100)}" dirty="0"`;591      if (r.bold) rPrAttrs += ' b="1"';592      if (r.italic) rPrAttrs += ' i="1"';593      if (r.underline) rPrAttrs += ' u="sng"';594      if (r.font && r.font !== 'Calibri') rPrAttrs += ` latin="${escXml(r.font)}" ea="${escXml(r.font)}"`;595      let fillXml = '';596      if (r.color && r.color !== '000000') {597        fillXml = `<a:solidFill><a:srgbClr val="${r.color}"/></a:solidFill>`;598      }599      runsXml += `<a:r><a:rPr ${rPrAttrs}>${fillXml}</a:rPr><a:t>${escXml(r.text)}</a:t></a:r>`;600    }601    parasXml += `<a:p>${pPr}${runsXml}</a:p>`;602  }603  const prst = el.shapeType === 'oval' ? 'ellipse' : 'rect';604  let fillXml = '';605  if (el.fill) fillXml = `<a:solidFill><a:srgbClr val="${el.fill}"/></a:solidFill>`;606  return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${name}"/><p:cNvSpPr txBox="${txBox}"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${Math.round(el.x)}" y="${Math.round(el.y)}"/><a:ext cx="${Math.round(el.width)}" cy="${Math.round(el.height)}"/></a:xfrm><a:prstGeom prst="${prst}"><a:avLst/></a:prstGeom>${fillXml}</p:spPr><p:txBody><a:bodyPr wrap="square" lIns="91440" tIns="45720" rIns="91440" bIns="45720" anchor="t"/><a:lstStyle/>${parasXml}</p:txBody></p:sp>`;607}608 609function buildPureShapeXml(el) {610  const id = el.id;611  const name = escXml(el.name);612  const prst = el.shapeType === 'oval' ? 'ellipse' : 'rect';613  let fillXml = '';614  if (el.fill) fillXml = `<a:solidFill><a:srgbClr val="${el.fill}"/></a:solidFill>`;615  let lineXml = '';616  if (el.line) lineXml = `<a:ln><a:solidFill><a:srgbClr val="${el.line}"/></a:solidFill></a:ln>`;617  return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${name}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${Math.round(el.x)}" y="${Math.round(el.y)}"/><a:ext cx="${Math.round(el.width)}" cy="${Math.round(el.height)}"/></a:xfrm><a:prstGeom prst="${prst}"><a:avLst/></a:prstGeom>${fillXml}${lineXml}</p:spPr></p:sp>`;618}619 620function buildPicXml(el) {621  const id = el.id;622  const name = escXml(el.name);623  // Para imagens, usamos o mesmo rId2 (precisaria ser dinâmico, mas simplificamos)624  return `<p:pic><p:nvPicPr><p:cNvPr id="${id}" name="${name}"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rId2"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${Math.round(el.x)}" y="${Math.round(el.y)}"/><a:ext cx="${Math.round(el.width)}" cy="${Math.round(el.height)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;625}626 627async function savePPTX() {628  let zip;629  if (state.zip) {630    zip = await state.zip.generateAsync({ type: 'blob' }).then(b => JSZip.loadAsync(b));631  } else {632    zip = createNewZip();633  }634 635  const slideCount = state.slides.length;636 637  // Atualizar/criar XML de cada slide638  for (let i = 0; i < slideCount; i++) {639    const slide = state.slides[i];640    const path = `ppt/slides/slide${i + 1}.xml`;641    if (slide.modified) {642      zip.file(path, buildSlideXml(slide));643    } else {644      zip.file(path, slide.originalXml);645    }646    // Criar rels do slide647    let rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>648<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">649<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>`;650    // Adicionar rels de imagem se necessário651    let imgRId = 2;652    for (const el of slide.elements) {653      if (el.type === 'image' && el.imgPath) {654        rels += `\n<Relationship Id="rId${imgRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="${escXml(el.imgPath)}"/>`;655        imgRId++;656      }657    }658    rels += `\n</Relationships>`;659    zip.file(`ppt/slides/_rels/slide${i + 1}.xml.rels`, rels);660  }661 662  // Remover slides antigos que não existem mais663  const toRemove = [];664  zip.forEach((path) => {665    const m = path.match(/^ppt\/slides\/slide(\d+)\.xml$/);666    if (m && parseInt(m[1]) > slideCount) toRemove.push(path);667    const m2 = path.match(/^ppt\/slides\/_rels\/slide(\d+)\.xml\.rels$/);668    if (m2 && parseInt(m2[1]) > slideCount) toRemove.push(path);669  });670  toRemove.forEach(p => zip.remove(p));671 672  // Atualizar presentation.xml673  const presXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>674<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">675<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId2"/></p:sldMasterIdLst>676<p:sldIdLst>${state.slides.map((_, i) => `<p:sldId id="${256 + i}" r:id="rId${i + 3}"/>`).join('\n')}</p:sldIdLst>677<p:sldSz cx="9144000" cy="6858000" type="screen4x3"/>678<p:notesSz cx="6858000" cy="9144000"/>679</p:presentation>`;680  zip.file('ppt/presentation.xml', presXml);681 682  // Atualizar presentation.xml.rels683  let presRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>684<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">685<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>686<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`;687  for (let i = 0; i < slideCount; i++) {688    presRels += `\n<Relationship Id="rId${i + 3}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide${i + 1}.xml"/>`;689  }690  presRels += `\n</Relationships>`;691  zip.file('ppt/_rels/presentation.xml.rels', presRels);692 693  // Atualizar [Content_Types].xml694  let contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>695<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">696<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>697<Default Extension="xml" ContentType="application/xml"/>698<Default Extension="png" ContentType="image/png"/>699<Default Extension="jpeg" ContentType="image/jpeg"/>700<Default Extension="jpg" ContentType="image/jpeg"/>701<Default Extension="gif" ContentType="image/gif"/>702<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>703<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>704<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>705<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>`;706  for (let i = 0; i < slideCount; i++) {707    contentTypes += `\n<Override PartName="/ppt/slides/slide${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`;708  }709  contentTypes += `\n</Types>`;710  zip.file('[Content_Types].xml', contentTypes);711 712  // Gerar e baixar713  const blob = await zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' });714  const url = URL.createObjectURL(blob);715  const a = document.createElement('a');716  a.href = url;717  a.download = state.fileName + '.pptx';718  a.click();719  URL.revokeObjectURL(url);720  toast('Arquivo salvo com sucesso!', 'success');721}722 723function createNewZip() {724  const zip = new JSZip();725  zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>726<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">727<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>728<Default Extension="xml" ContentType="application/xml"/>729<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>730<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>731<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>732<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>733<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>734</Types>`);735  zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>736<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">737<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>738</Relationships>`);739  // Slide master mínimo740  zip.file('ppt/slideMasters/slideMaster1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>741<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">742<p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>743<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>744<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>745</p:sldMaster>`);746  zip.file('ppt/slideMasters/_rels/slideMaster1.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>747<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">748<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>749<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>750</Relationships>`);751  // Slide layout mínimo752  zip.file('ppt/slideLayouts/slideLayout1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>753<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">754<p:cSld name="Em Branco"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>755<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>756</p:sldLayout>`);757  zip.file('ppt/slideLayouts/_rels/slideLayout1.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>758<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">759<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>760</Relationships>`);761  // Tema mínimo762  zip.file('ppt/theme/theme1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>763<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Tema">764<a:themeElements><a:clrScheme name="Escritório"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="44546A"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="Padrão"><a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="Padrão"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements>765<a:objectDefaults/><a:extraClrSchemeLst/>766</a:theme>`);767  return zip;768}769 770// ============================================================771// RENDERIZAÇÃO772// ============================================================773function renderAll() {774  renderSlideList();775  renderSlide();776  renderProps();777  updateStatus();778}779 780function renderSlideList() {781  const list = document.getElementById('slides-list');782  list.innerHTML = '';783  state.slides.forEach((slide, i) => {784    const thumb = document.createElement('div');785    thumb.className = 'slide-thumb' + (i === state.currentSlide ? ' active' : '');786    thumb.style.background = slide.background || '#fff';787 788    const content = document.createElement('div');789    content.className = 'slide-thumb-content';790    const thumbScale = 160 / slide.width;791 792    slide.elements.forEach(el => {793      const d = document.createElement('div');794      d.className = 'thumb-el';795      d.style.left = (el.x * thumbScale) + 'px';796      d.style.top = (el.y * thumbScale) + 'px';797      d.style.width = (el.width * thumbScale) + 'px';798      d.style.height = (el.height * thumbScale) + 'px';799 800      if (el.type === 'text') {801        const txt = el.paragraphs.map(p => p.runs.map(r => r.text).join('')).join(' ');802        d.textContent = txt || ' ';803        if (el.fill) d.style.background = '#' + el.fill;804      } else if (el.type === 'shape') {805        d.style.background = el.fill ? '#' + el.fill : '#ccc';806        if (el.shapeType === 'oval') d.style.borderRadius = '50%';807      } else if (el.type === 'image' && el.dataUrl) {808        d.innerHTML = `<img src="${el.dataUrl}" style="width:100%;height:100%;object-fit:contain;">`;809      }810      content.appendChild(d);811    });812 813    // Escalar o conteúdo para caber no thumb814    const thumbH = thumb.offsetWidth ? thumb.offsetWidth * 0.75 : 120;815    content.style.transform = `scale(${thumbH / (slide.height * thumbScale)})`;816    content.style.width = (slide.width * thumbScale) + 'px';817    content.style.height = (slide.height * thumbScale) + 'px';818 819    thumb.appendChild(content);820 821    const num = document.createElement('div');822    num.className = 'slide-thumb-num';823    num.textContent = i + 1;824    thumb.appendChild(num);825 826    thumb.addEventListener('click', () => {827      if (state.editing) exitEditing();828      state.currentSlide = i;829      state.selectedId = null;830      renderAll();831    });832 833    list.appendChild(thumb);834  });835}836 837function renderSlide() {838  if (state.slides.length === 0) return;839 840  const slide = state.slides[state.currentSlide];841  const area = document.getElementById('editor-area');842  const wrapper = document.getElementById('slide-wrapper');843  const canvas = document.getElementById('slide-canvas');844 845  const availW = area.clientWidth - 60;846  const availH = area.clientHeight - 60;847  const aspect = slide.width / slide.height;848 849  let cw, ch;850  if (availW / availH > aspect) {851    ch = availH;852    cw = ch * aspect;853  } else {854    cw = availW;855    ch = cw / aspect;856  }857 858  state.scale = cw / slide.width;859  const fontScale = 12700 * state.scale; // pontos para pixels860 861  canvas.style.width = cw + 'px';862  canvas.style.height = ch + 'px';863  canvas.style.background = slide.background || '#fff';864  wrapper.style.display = 'block';865 866  canvas.innerHTML = '';867 868  slide.elements.forEach(el => {869    const div = document.createElement('div');870    div.className = 'slide-element';871    if (el.id === state.selectedId) div.classList.add('selected');872    div.dataset.id = el.id;873    div.style.left = (el.x * state.scale) + 'px';874    div.style.top = (el.y * state.scale) + 'px';875    div.style.width = (el.width * state.scale) + 'px';876    div.style.height = (el.height * state.scale) + 'px';877 878    if (el.type === 'text') {879      if (el.fill) div.style.background = '#' + el.fill;880      if (el.shapeType === 'oval') div.style.borderRadius = '50%';881      div.innerHTML = el.paragraphs.map(p => {882        const align = p.align || 'left';883        return `<p style="margin:0;text-align:${align};line-height:1.2;">${p.runs.map(r => {884          let s = `font-size:${Math.max(1, r.size * fontScale)}px;`;885          if (r.bold) s += 'font-weight:700;';886          if (r.italic) s += 'font-style:italic;';887          if (r.underline) s += 'text-decoration:underline;';888          if (r.color) s += `color:#${r.color};`;889          if (r.font) s += `font-family:'${r.font}',sans-serif;`;890          return `<span style="${s}">${escHtml(r.text)}</span>`;891        }).join('')}</p>`;892      }).join('');893    } else if (el.type === 'shape') {894      div.style.background = el.fill ? '#' + el.fill : 'transparent';895      if (el.line) div.style.border = `2px solid #${el.line}`;896      if (el.shapeType === 'oval') div.style.borderRadius = '50%';897    } else if (el.type === 'image' && el.dataUrl) {898      div.innerHTML = `<img src="${el.dataUrl}" style="width:100%;height:100%;object-fit:contain;pointer-events:none;">`;899    }900 901    // Handles de redimensionamento902    ['nw','n','ne','e','se','s','sw','w'].forEach(dir => {903      const h = document.createElement('div');904      h.className = 'resize-handle ' + dir;905      h.dataset.dir = dir;906      div.appendChild(h);907    });908 909    canvas.appendChild(div);910  });911}912 913function renderProps() {914  const cont = document.getElementById('props-content');915  const slide = state.slides[state.currentSlide];916  if (!slide) { cont.innerHTML = '<p class="no-selection">Nenhum slide</p>'; return; }917 918  const el = slide.elements.find(e => e.id === state.selectedId);919  if (!el) {920    cont.innerHTML = `921      <div class="prop-section">922        <div class="prop-row"><span class="prop-label" style="width:auto;color:var(--text-dim);font-size:11px;">Slide ${state.currentSlide + 1}</span></div>923        <div class="prop-row"><span class="prop-label">Cor</span><input type="color" class="prop-color" id="prop-bg-color" value="${(slide.background || '#ffffff').replace('#','')}"></div>924      </div>925      <p class="no-selection">Selecione um elemento para editar</p>`;926    const bgInput = document.getElementById('prop-bg-color');927    if (bgInput) bgInput.addEventListener('input', (e) => {928      slide.background = '#' + e.target.value;929      slide.modified = true;930      renderSlide();931      renderSlideList();932    });933    return;934  }935 936  let html = `<div class="prop-section">937    <div style="font-size:12px;color:var(--text-dim);margin-bottom:8px;font-weight:500;">${escHtml(el.name || 'Elemento')}</div>938    <div class="prop-row"><span class="prop-label">X</span><input type="number" class="prop-input" id="prop-x" value="${Math.round(el.x / 9144)}" step="1"></div>939    <div class="prop-row"><span class="prop-label">Y</span><input type="number" class="prop-input" id="prop-y" value="${Math.round(el.y / 9144)}" step="1"></div>940    <div class="prop-row"><span class="prop-label">L</span><input type="number" class="prop-input" id="prop-w" value="${Math.round(el.width / 9144)}" step="1" min="1"></div>941    <div class="prop-row"><span class="prop-label">A</span><input type="number" class="prop-input" id="prop-h" value="${Math.round(el.height / 9144)}" step="1" min="1"></div>942  </div>`;943 944  if (el.type === 'text') {945    const firstRun = el.paragraphs[0]?.runs[0] || {};946    html += `<div class="prop-section">947      <div style="font-size:11px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">Texto</div>948      <div class="prop-row"><span class="prop-label"><i class="fas fa-font" style="font-size:10px;"></i></span><select class="prop-input" id="prop-font">${['Calibri','Arial','Times New Roman','Courier New','Georgia','Verdana','Tahoma','Impact','Comic Sans MS'].map(f => `<option value="${f}" ${firstRun.font === f ? 'selected' : ''}>${f}</option>`).join('')}</select></div>949      <div class="prop-row"><span class="prop-label" style="font-size:10px;">pt</span><input type="number" class="prop-input" id="prop-fsize" value="${firstRun.size || 18}" min="6" max="200"></div>950      <div class="prop-row"><span class="prop-label">Cor</span><input type="color" class="prop-color" id="prop-fcolor" value="#${firstRun.color || '000000'}"></div>951      <div class="prop-row" style="gap:4px;">952        <button class="prop-btn ${firstRun.bold ? 'active' : ''}" id="prop-bold" title="Negrito"><i class="fas fa-bold"></i></button>953        <button class="prop-btn ${firstRun.italic ? 'active' : ''}" id="prop-italic" title="Itálico"><i class="fas fa-italic"></i></button>954        <button class="prop-btn ${firstRun.underline ? 'active' : ''}" id="prop-underline" title="Sublinhado"><i class="fas fa-underline"></i></button>955      </div>956      <div class="prop-row" style="gap:4px;">957        <button class="prop-btn ${el.paragraphs[0]?.align === 'left' ? 'active' : ''}" id="prop-al"><i class="fas fa-align-left"></i></button>958        <button class="prop-btn ${el.paragraphs[0]?.align === 'center' ? 'active' : ''}" id="prop-ac"><i class="fas fa-align-center"></i></button>959        <button class="prop-btn ${el.paragraphs[0]?.align === 'right' ? 'active' : ''}" id="prop-ar"><i class="fas fa-align-right"></i></button>960      </div>961    </div>`;962  }963 964  if (el.type === 'shape' || (el.type === 'text' && el.fill)) {965    html += `<div class="prop-section">966      <div style="font-size:11px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">Preenchimento</div>967      <div class="prop-row"><span class="prop-label">Cor</span><input type="color" class="prop-color" id="prop-fill" value="#${el.fill || '4472C4'}"></div>968    </div>`;969  }970 971  html += `<div class="prop-section">972    <button class="prop-btn danger" id="prop-delete" style="width:100%;"><i class="fas fa-trash"></i> Excluir Elemento</button>973  </div>`;974 975  cont.innerHTML = html;976 977  // Bind eventos das propriedades978  const bind = (id, fn) => { const e = document.getElementById(id); if (e) e.addEventListener('input', fn); };979  const bindClick = (id, fn) => { const e = document.getElementById(id); if (e) e.addEventListener('click', fn); };980 981  bind('prop-x', e => { el.x = parseFloat(e.target.value) * 9144; slide.modified = true; renderSlide(); });982  bind('prop-y', e => { el.y = parseFloat(e.target.value) * 9144; slide.modified = true; renderSlide(); });983  bind('prop-w', e => { el.width = Math.max(9144, parseFloat(e.target.value) * 9144); slide.modified = true; renderSlide(); });984  bind('prop-h', e => { el.height = Math.max(9144, parseFloat(e.target.value) * 9144); slide.modified = true; renderSlide(); });985 986  if (el.type === 'text') {987    bind('prop-font', e => { el.paragraphs.forEach(p => p.runs.forEach(r => r.font = e.target.value)); slide.modified = true; renderSlide(); syncToolbar(); });988    bind('prop-fsize', e => { const sz = parseFloat(e.target.value) || 18; el.paragraphs.forEach(p => p.runs.forEach(r => r.size = sz)); slide.modified = true; renderSlide(); syncToolbar(); });989    bind('prop-fcolor', e => { const c = e.target.value.replace('#',''); el.paragraphs.forEach(p => p.runs.forEach(r => r.color = c)); slide.modified = true; renderSlide(); syncToolbar(); });990    bindClick('prop-bold', () => { const b = !el.paragraphs[0].runs[0].bold; el.paragraphs.forEach(p => p.runs.forEach(r => r.bold = b)); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });991    bindClick('prop-italic', () => { const v = !el.paragraphs[0].runs[0].italic; el.paragraphs.forEach(p => p.runs.forEach(r => r.italic = v)); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });992    bindClick('prop-underline', () => { const v = !el.paragraphs[0].runs[0].underline; el.paragraphs.forEach(p => p.runs.forEach(r => r.underline = v)); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });993    bindClick('prop-al', () => { el.paragraphs.forEach(p => p.align = 'left'); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });994    bindClick('prop-ac', () => { el.paragraphs.forEach(p => p.align = 'center'); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });995    bindClick('prop-ar', () => { el.paragraphs.forEach(p => p.align = 'right'); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });996  }997 998  if (el.type === 'shape' || (el.type === 'text' && el.fill !== undefined)) {999    bind('prop-fill', e => { el.fill = e.target.value.replace('#',''); slide.modified = true; renderSlide(); });1000  }1001 1002  bindClick('prop-delete', () => {1003    pushHistory();1004    const si = state.slides[state.currentSlide];1005    si.elements = si.elements.filter(e => e.id !== el.id);1006    si.modified = true;1007    state.selectedId = null;1008    renderAll();1009  });1010}1011 1012function syncToolbar() {1013  const slide = state.slides[state.currentSlide];1014  if (!slide) return;1015  const el = slide.elements.find(e => e.id === state.selectedId);1016  if (!el || el.type !== 'text') return;1017 1018  const run = el.paragraphs[0]?.runs[0] || {};1019  document.getElementById('font-select').value = run.font || 'Calibri';1020  document.getElementById('font-size').value = run.size || 18;1021  document.getElementById('font-color').value = '#' + (run.color || '000000');1022  document.getElementById('btn-bold').classList.toggle('active', !!run.bold);1023  document.getElementById('btn-italic').classList.toggle('active', !!run.italic);1024  document.getElementById('btn-underline').classList.toggle('active', !!run.underline);1025  document.getElementById('btn-align-l').classList.toggle('active', el.paragraphs[0]?.align === 'left');1026  document.getElementById('btn-align-c').classList.toggle('active', el.paragraphs[0]?.align === 'center');1027  document.getElementById('btn-align-r').classList.toggle('active', el.paragraphs[0]?.align === 'right');1028}1029 1030function updateStatus() {1031  document.getElementById('status-file').innerHTML = `<i class="fas fa-file"></i> ${escHtml(state.fileName)}`;1032  document.getElementById('status-slide').innerHTML = `<i class="fas fa-layer-group"></i> Slide ${state.slides.length > 0 ? state.currentSlide + 1 : '-'}/${state.slides.length}`;1033  const slide = state.slides[state.currentSlide];1034  document.getElementById('status-elems').innerHTML = `<i class="fas fa-shapes"></i> ${slide ? slide.elements.length : 0} elementos`;1035  document.getElementById('status-zoom').innerHTML = `<i class="fas fa-search"></i> ${Math.round(state.scale * 914400 / 96)}%`;1036}1037 1038// ============================================================1039// INTERAÇÃO (SELEÇÃO, DRAG, RESIZE, EDIÇÃO)1040// ============================================================1041function exitEditing() {1042  if (!state.editing) return;1043  state.editing = false;1044  const slide = state.slides[state.currentSlide];1045  const el = slide?.elements.find(e => e.id === state.selectedId);1046  if (el && el.type === 'text') {1047    const domEl = document.querySelector(`.slide-element[data-id="${el.id}"]`);1048    if (domEl) {1049      // Extrair texto editado1050      const paragraphs = [];1051      domEl.querySelectorAll('p').forEach(p => {1052        const para = { align: p.style.textAlign || 'left', runs: [] };1053        const firstRun = el.paragraphs[0]?.runs[0] || { bold:false, italic:false, underline:false, size:18, color:'000000', font:'Calibri' };1054 1055        if (p.children.length === 0) {1056          para.runs.push({ ...firstRun, text: p.textContent || '' });1057        } else {1058          p.childNodes.forEach(node => {1059            if (node.nodeType === Node.TEXT_NODE) {1060              if (node.textContent) para.runs.push({ ...firstRun, text: node.textContent });1061            } else if (node.nodeType === Node.ELEMENT_NODE) {1062              const s = node.style;1063              const run = {1064                text: node.textContent || '',1065                bold: s.fontWeight === '700' || s.fontWeight === 'bold',1066                italic: s.fontStyle === 'italic',1067                underline: s.textDecoration === 'underline',1068                size: parseFloat(s.fontSize) / (12700 * state.scale) || firstRun.size,1069                color: (s.color || '').replace('#','') || firstRun.color,1070                font: (s.fontFamily || '').replace(/['"]/g,'') || firstRun.font1071              };1072              para.runs.push(run);1073            }1074          });1075        }1076        if (para.runs.length === 0) para.runs.push({ ...firstRun, text: '' });1077        paragraphs.push(para);1078      });1079      if (paragraphs.length > 0) {1080        el.paragraphs = paragraphs;1081        slide.modified = true;1082      }1083    }1084  }1085  renderSlide();1086}1087 1088function initEditorEvents() {1089  const canvas = document.getElementById('slide-canvas');1090 1091  canvas.addEventListener('mousedown', (e) => {1092    const handle = e.target.closest('.resize-handle');1093    const elemDiv = e.target.closest('.slide-element');1094 1095    if (handle && elemDiv) {1096      // Iniciar resize1097      e.preventDefault();1098      e.stopPropagation();1099      state.resizing = true;1100      state.resizeDir = handle.dataset.dir;1101      state.selectedId = parseInt(elemDiv.dataset.id);1102      const slide = state.slides[state.currentSlide];1103      const el = slide.elements.find(e => e.id === state.selectedId);1104      state.dragStart = { x: e.clientX, y: e.clientY };1105      state.elemStart = { x: el.x, y: el.y, w: el.width, h: el.height };1106      pushHistory();1107      return;1108    }1109 1110    if (elemDiv) {1111      const id = parseInt(elemDiv.dataset.id);1112      if (state.editing && state.selectedId === id) return; // Já está editando1113 1114      if (state.editing) exitEditing();1115 1116      state.selectedId = id;1117      state.dragging = true;1118      state.dragStart = { x: e.clientX, y: e.clientY };1119      const slide = state.slides[state.currentSlide];1120      const el = slide.elements.find(e => e.id === id);1121      state.elemStart = { x: el.x, y: el.y, w: el.width, h: el.height };1122      pushHistory();1123      renderSlide();1124      renderProps();1125      syncToolbar();1126      return;1127    }1128 1129    // Clicou no vazio1130    if (state.editing) exitEditing();1131    state.selectedId = null;1132    renderSlide();1133    renderProps();1134  });1135 1136  canvas.addEventListener('dblclick', (e) => {1137    const elemDiv = e.target.closest('.slide-element');1138    if (!elemDiv) return;1139    const id = parseInt(elemDiv.dataset.id);1140    const slide = state.slides[state.currentSlide];1141    const el = slide.elements.find(e => e.id === id);1142    if (!el || el.type !== 'text') return;1143 1144    state.editing = true;1145    state.selectedId = id;1146    elemDiv.classList.add('editing');1147    elemDiv.contentEditable = 'true';1148    elemDiv.focus();1149 1150    // Selecionar todo o texto1151    const range = document.createRange();1152    range.selectNodeContents(elemDiv);1153    const sel = window.getSelection();1154    sel.removeAllRanges();1155    sel.addRange(range);1156  });1157 1158  document.addEventListener('mousemove', (e) => {1159    if (state.dragging && state.selectedId !== null) {1160      const dx = (e.clientX - state.dragStart.x) / state.scale;1161      const dy = (e.clientY - state.dragStart.y) / state.scale;1162      const slide = state.slides[state.currentSlide];1163      const el = slide.elements.find(e => e.id === state.selectedId);1164      if (el) {1165        el.x = Math.max(0, state.elemStart.x + dx);1166        el.y = Math.max(0, state.elemStart.y + dy);1167        slide.modified = true;1168        // Atualizar posição diretamente no DOM (sem re-render completo)1169        const domEl = document.querySelector(`.slide-element[data-id="${el.id}"]`);1170        if (domEl) {1171          domEl.style.left = (el.x * state.scale) + 'px';1172          domEl.style.top = (el.y * state.scale) + 'px';1173        }1174      }1175    }1176 1177    if (state.resizing && state.selectedId !== null) {1178      const dx = (e.clientX - state.dragStart.x) / state.scale;1179      const dy = (e.clientY - state.dragStart.y) / state.scale;1180      const slide = state.slides[state.currentSlide];1181      const el = slide.elements.find(e => e.id === state.selectedId);1182      if (!el) return;1183 1184      const minS = 91440; // tamanho mínimo1185      const dir = state.resizeDir;1186      let nx = state.elemStart.x, ny = state.elemStart.y;1187      let nw = state.elemStart.w, nh = state.elemStart.h;1188 1189      if (dir.includes('e')) nw = Math.max(minS, state.elemStart.w + dx);1190      if (dir.includes('w')) { nw = Math.max(minS, state.elemStart.w - dx); nx = state.elemStart.x + state.elemStart.w - nw; }1191      if (dir.includes('s')) nh = Math.max(minS, state.elemStart.h + dy);1192      if (dir.includes('n')) { nh = Math.max(minS, state.elemStart.h - dy); ny = state.elemStart.y + state.elemStart.h - nh; }1193 1194      el.x = nx; el.y = ny; el.width = nw; el.height = nh;1195      slide.modified = true;1196 1197      const domEl = document.querySelector(`.slide-element[data-id="${el.id}"]`);1198      if (domEl) {1199        domEl.style.left = (el.x * state.scale) + 'px';1200        domEl.style.top = (el.y * state.scale) + 'px';

Showing the first 1,200 of 1546 lines. Download the file for the rest.