sagadeck 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/SKILL.md +148 -0
  4. package/bin/sagadeck.js +511 -0
  5. package/docs/ANDAMENTO.md +1255 -0
  6. package/docs/EXPERIENCIAS.md +39 -0
  7. package/docs/PLANO-EXPERIENCIAS.md +39 -0
  8. package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
  9. package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
  10. package/docs/REFERENCIA.md +1131 -0
  11. package/package.json +59 -0
  12. package/src/adaptive-layouts.js +18 -0
  13. package/src/ai/art-direction.js +8 -0
  14. package/src/ai/commands.js +130 -0
  15. package/src/ai/context.js +245 -0
  16. package/src/ai/deck-ai.js +1135 -0
  17. package/src/ai/directions.js +74 -0
  18. package/src/ai/document-materials.js +49 -0
  19. package/src/ai/document-visuals.js +215 -0
  20. package/src/ai/evaluation.js +31 -0
  21. package/src/ai/ground.js +55 -0
  22. package/src/ai/llm.js +292 -0
  23. package/src/ai/quality.js +51 -0
  24. package/src/ai/relay.js +45 -0
  25. package/src/ai/staged-generation.js +24 -0
  26. package/src/ai/transform.js +858 -0
  27. package/src/ai/usage.js +17 -0
  28. package/src/ai/variety.js +80 -0
  29. package/src/ai/video-frames.js +24 -0
  30. package/src/ai/video-generation.js +126 -0
  31. package/src/ai/web-operation.js +31 -0
  32. package/src/algo-catalog.js +153 -0
  33. package/src/algo-trace.js +139 -0
  34. package/src/api-client.js +514 -0
  35. package/src/api-collections.js +14 -0
  36. package/src/api-demo.js +222 -0
  37. package/src/aspect.js +40 -0
  38. package/src/build.js +323 -0
  39. package/src/carousel.js +56 -0
  40. package/src/chrome.js +65 -0
  41. package/src/code-highlight.js +68 -0
  42. package/src/code-lab-browser.js +22 -0
  43. package/src/code-lab.js +3 -0
  44. package/src/code-language.js +52 -0
  45. package/src/csv.js +74 -0
  46. package/src/decision-lab.js +20 -0
  47. package/src/deck-file.js +119 -0
  48. package/src/diagram/napkin-examples.js +13 -0
  49. package/src/diagram/napkin.js +539 -0
  50. package/src/docx.js +128 -0
  51. package/src/dynamics/git-sim.js +200 -0
  52. package/src/dynamics/layouts.js +112 -0
  53. package/src/elements.js +554 -0
  54. package/src/experiences.js +124 -0
  55. package/src/exploration.js +31 -0
  56. package/src/export/browser.js +53 -0
  57. package/src/export/estudo.js +67 -0
  58. package/src/export/pptx.js +475 -0
  59. package/src/export/roteiro.js +40 -0
  60. package/src/export/shots.js +182 -0
  61. package/src/figures/charts.js +383 -0
  62. package/src/figures/diagrams.js +117 -0
  63. package/src/figures/icons.js +166 -0
  64. package/src/figures/material-pictos.js +20 -0
  65. package/src/figures/pictos.js +255 -0
  66. package/src/figures/points.js +106 -0
  67. package/src/figures/qr.js +29 -0
  68. package/src/figures/ufmap.js +48 -0
  69. package/src/fiscal/autofix.js +377 -0
  70. package/src/fiscal/normalize.js +115 -0
  71. package/src/graph-lab.js +24 -0
  72. package/src/hologram.js +30 -0
  73. package/src/identity.js +86 -0
  74. package/src/import/crop.js +59 -0
  75. package/src/import/index.js +132 -0
  76. package/src/import/legacy-word.js +42 -0
  77. package/src/import/merge.js +45 -0
  78. package/src/import/office.js +123 -0
  79. package/src/import/omml.js +69 -0
  80. package/src/import/pdf-render.js +56 -0
  81. package/src/import/pptx.js +701 -0
  82. package/src/import/xml.js +42 -0
  83. package/src/infographic.js +401 -0
  84. package/src/layouts.js +723 -0
  85. package/src/lessons.js +171 -0
  86. package/src/library.js +399 -0
  87. package/src/markup.js +116 -0
  88. package/src/master.js +214 -0
  89. package/src/mcp/server.js +371 -0
  90. package/src/meeting.js +42 -0
  91. package/src/motion.js +56 -0
  92. package/src/package.js +116 -0
  93. package/src/playground.js +19 -0
  94. package/src/portal-scene.js +20 -0
  95. package/src/preferences.js +61 -0
  96. package/src/protect.js +51 -0
  97. package/src/purpose.js +24 -0
  98. package/src/pytrace.js +1092 -0
  99. package/src/research/duckduckgo.js +73 -0
  100. package/src/research/research.js +202 -0
  101. package/src/research/svg-image.js +17 -0
  102. package/src/research/visuals.js +52 -0
  103. package/src/runtime/api-collection-ui.js +5 -0
  104. package/src/runtime/api-collections.js +28 -0
  105. package/src/runtime/api-core.js +530 -0
  106. package/src/runtime/api-ui.js +966 -0
  107. package/src/runtime/base.css +1774 -0
  108. package/src/runtime/calc.js +139 -0
  109. package/src/runtime/code-lab.js +371 -0
  110. package/src/runtime/decision-lab.js +49 -0
  111. package/src/runtime/diagram.js +341 -0
  112. package/src/runtime/fit.js +268 -0
  113. package/src/runtime/fonts/caveat.css +2 -0
  114. package/src/runtime/fonts/comic-neue.css +3 -0
  115. package/src/runtime/fonts/fredoka.css +2 -0
  116. package/src/runtime/fonts/index.json +10 -0
  117. package/src/runtime/fonts/inter.css +2 -0
  118. package/src/runtime/fonts/patrick-hand.css +2 -0
  119. package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
  120. package/src/runtime/fonts/press-start-2p.css +2 -0
  121. package/src/runtime/fonts/vt323.css +2 -0
  122. package/src/runtime/formula.js +148 -0
  123. package/src/runtime/graph-lab.js +72 -0
  124. package/src/runtime/meeting.css +7 -0
  125. package/src/runtime/meeting.js +10 -0
  126. package/src/runtime/motion.js +15 -0
  127. package/src/runtime/playground.js +11 -0
  128. package/src/runtime/points.js +19 -0
  129. package/src/runtime/portal-scene.css +6 -0
  130. package/src/runtime/portal-scene.js +22 -0
  131. package/src/runtime/runtime.js +827 -0
  132. package/src/runtime/science.js +89 -0
  133. package/src/runtime/skins/aluminio.css +31 -0
  134. package/src/runtime/skins/arcade.css +25 -0
  135. package/src/runtime/skins/aurora.css +17 -0
  136. package/src/runtime/skins/bauhaus.css +19 -0
  137. package/src/runtime/skins/editorial.css +19 -0
  138. package/src/runtime/skins/jornal.css +17 -0
  139. package/src/runtime/skins/manual-noite.css +14 -0
  140. package/src/runtime/skins/manual.css +14 -0
  141. package/src/runtime/skins/noite.css +21 -0
  142. package/src/runtime/skins/oceano.css +17 -0
  143. package/src/runtime/skins/pop.css +23 -0
  144. package/src/runtime/skins/prata.css +18 -0
  145. package/src/runtime/skins/prisma.css +28 -0
  146. package/src/runtime/skins/rabisco.css +18 -0
  147. package/src/runtime/skins/relevo.css +33 -0
  148. package/src/runtime/skins/sinal.css +16 -0
  149. package/src/runtime/skins/terminal.css +23 -0
  150. package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
  151. package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
  152. package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
  153. package/src/runtime/vendor/katex.css +1 -0
  154. package/src/runtime/vendor/mermaid.min.js +3636 -0
  155. package/src/runtime/vendor/plotly.min.js +3620 -0
  156. package/src/runtime/video-player.js +35 -0
  157. package/src/science.js +162 -0
  158. package/src/studio/ai-response.js +36 -0
  159. package/src/studio/api-routes.js +152 -0
  160. package/src/studio/assets/contexto/leia-me.md +12 -0
  161. package/src/studio/assets/contexto/vendas.csv +7 -0
  162. package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
  163. package/src/studio/assets/imagens/colecao-cor.png +0 -0
  164. package/src/studio/assets/imagens/colecao-equipe.png +0 -0
  165. package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
  166. package/src/studio/code-routes.js +32 -0
  167. package/src/studio/command-approval.js +48 -0
  168. package/src/studio/demo-decks.js +98 -0
  169. package/src/studio/direction-routes.js +29 -0
  170. package/src/studio/errors.js +65 -0
  171. package/src/studio/exploration-demo.js +17 -0
  172. package/src/studio/exporting.js +105 -0
  173. package/src/studio/generation-report.js +10 -0
  174. package/src/studio/instance.js +48 -0
  175. package/src/studio/layout-samples.js +152 -0
  176. package/src/studio/project.js +371 -0
  177. package/src/studio/public/api-collections.js +28 -0
  178. package/src/studio/public/app.js +5265 -0
  179. package/src/studio/public/art-preview.js +10 -0
  180. package/src/studio/public/calc-fields.js +11 -0
  181. package/src/studio/public/chat-progress.js +47 -0
  182. package/src/studio/public/developer-fields.js +22 -0
  183. package/src/studio/public/explorer.js +443 -0
  184. package/src/studio/public/history.js +58 -0
  185. package/src/studio/public/index.html +867 -0
  186. package/src/studio/public/inspector.js +270 -0
  187. package/src/studio/public/lab-fields.js +5 -0
  188. package/src/studio/public/library.css +275 -0
  189. package/src/studio/public/library.html +55 -0
  190. package/src/studio/public/library.js +471 -0
  191. package/src/studio/public/merge-decks.js +155 -0
  192. package/src/studio/public/review-ui.js +100 -0
  193. package/src/studio/public/screenshot-editor.js +140 -0
  194. package/src/studio/public/share-ui.js +72 -0
  195. package/src/studio/public/slide-clipboard.js +23 -0
  196. package/src/studio/public/slide-form.js +1263 -0
  197. package/src/studio/public/studio-awareness.js +154 -0
  198. package/src/studio/public/studio-next.css +303 -0
  199. package/src/studio/public/style.css +1262 -0
  200. package/src/studio/public/ui-icons.js +2 -0
  201. package/src/studio/public/viewers.js +372 -0
  202. package/src/studio/public/visual-editor.js +326 -0
  203. package/src/studio/public/workspace.js +18 -0
  204. package/src/studio/public-files.js +1 -0
  205. package/src/studio/request-helpers.js +41 -0
  206. package/src/studio/server.js +1583 -0
  207. package/src/studio/share-routes.js +146 -0
  208. package/src/studio/slide-copy-routes.js +26 -0
  209. package/src/studio/slide-copy.js +30 -0
  210. package/src/studio/snapshot.js +160 -0
  211. package/src/studio/style-routes.js +103 -0
  212. package/src/studio/template-collections.js +97 -0
  213. package/src/studio/versions.js +95 -0
  214. package/src/studio/video-download.js +29 -0
  215. package/src/studio/visual-keys.js +52 -0
  216. package/src/studio/workspaces.js +39 -0
  217. package/src/table.js +93 -0
  218. package/src/templates/scaffold.js +372 -0
  219. package/src/themes.js +604 -0
  220. package/src/trace-view.js +450 -0
  221. package/src/uid.js +19 -0
  222. package/src/video-player.js +40 -0
  223. package/src/visual-edits.js +99 -0
  224. package/src/ws.js +134 -0
  225. package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
  226. package/templates/cenario/imagens/cidade.svg +30 -0
  227. package/templates/cenario/imagens/pessoa.svg +15 -0
  228. package/templates/cenario-e-ciencia.yaml +47 -0
  229. package/templates/ensaio-api.yaml +160 -0
  230. package/templates/exemplo-alegre.yaml +75 -0
  231. package/templates/exemplo-keynote.yaml +90 -0
  232. package/templates/exemplo-napkin.yaml +139 -0
  233. package/templates/exemplo.yaml +228 -0
@@ -0,0 +1,326 @@
1
+ /* Ajustes locais preservam o layout automático. Não intercepta digitação nem os gráficos.
2
+ Seleção como no Google Slides/Canva: um clique em qualquer ponto do objeto seleciona (texto também); clicar de novo
3
+ no texto selecionado (ou duplo clique) escreve; arrastar numa área vazia desenha um retângulo que seleciona vários;
4
+ Shift+clique soma. Arrastar, setas, Delete e a barra valem para todos os selecionados. */
5
+ (function () {
6
+ let active = false, root, slide, commit, sel = [], host, toolbar, dragging, beforeDrag, pendingWrite, marquee, styleClip = null, pendingSolo = null;
7
+ // travado (a pessoa travou, ou é decoração do original: moldura, logos): o clique atravessa; só pela lista de objetos
8
+ const locked = (n) => n?.hasAttribute('data-locked');
9
+ // o que "copiar estilo" leva (aparência, não posição nem conteúdo)
10
+ const STYLE_KEYS = ['size','color','weight','italic','align','lineHeight','letterSpacing','uppercase','opacity','shadow','radius','stroke','strokeWidth','fill'];
11
+ const primary = () => sel[sel.length - 1] || null;
12
+ const listeners = []; // o inspetor do Studio acompanha a seleção
13
+ let restoring = false; // o slide redesenhou e a seleção voltou sozinha (não foi a pessoa que clicou)
14
+ const notify = () => listeners.forEach((fn) => { try { fn(sel.slice(), restoring ? "restore" : "user"); } catch {} });
15
+ const clone = x => JSON.parse(JSON.stringify(x));
16
+ const button = (label,fn,className='rbtn rbtn-lg',icon='') => { const b=document.createElement('button');b.type='button';b.className=className;if(icon){b.innerHTML=`<i class="ic" data-ic="${icon}"></i><span></span>`;b.querySelector('span').textContent=label;}else b.textContent=label;b.onclick=fn;return b; };
17
+ // Formas do menu: as mesmas que o motor desenha (src/elements.js); a prévia usa o mesmo desenho 100×100
18
+ const SHAPES = [
19
+ ['rect','Retângulo','<rect x="4" y="16" width="92" height="68"/>'],['rounded','Arredondado','<rect x="4" y="16" width="92" height="68" rx="16"/>'],
20
+ ['circle','Elipse','<ellipse cx="50" cy="50" rx="46" ry="38"/>'],['pill','Pílula','<rect x="4" y="28" width="92" height="44" rx="22"/>'],
21
+ ['line','Linha','<rect x="4" y="47" width="92" height="6" rx="3"/>'],['triangle','Triângulo','<polygon points="50,6 96,94 4,94"/>'],
22
+ ['diamond','Losango','<polygon points="50,4 96,50 50,96 4,50"/>'],['hexagon','Hexágono','<polygon points="27,8 73,8 96,50 73,92 27,92 4,50"/>'],
23
+ ['star','Estrela','<polygon points="50,4 61,37 96,37 68,58 79,92 50,71 21,92 32,58 4,37 39,37"/>'],['arrow','Seta','<polygon points="4,34 60,34 60,12 96,50 60,88 60,66 4,66"/>'],
24
+ ['chevron','Chevron','<polygon points="4,14 68,14 96,50 68,86 4,86 30,50"/>'],['bubble','Balão','<path d="M12,10 H88 Q96,10 96,18 V62 Q96,70 88,70 H42 L24,92 L28,70 H12 Q4,70 4,62 V18 Q4,10 12,10 Z"/>'],
25
+ ];
26
+ const shapeSize = k => k==='line' ? {w:420,h:8,thickness:8} : k==='circle' ? {w:280,h:280} : k==='pill' ? {w:380,h:120} : {w:320,h:240};
27
+ function saveBefore() {} // o desfazer é o do Studio (history.js): cada gravação vira um passo
28
+ function edit(el = primary()) { return (slide.visualEdits ||= {})[el.dataset.vkey] ||= {}; }
29
+ function finish() { commit?.(); }
30
+ function change(fn) { if(!sel.length)return;saveBefore();sel.forEach(el=>fn(edit(el)));finish(); }
31
+ // retângulo do objeto em px do slide (o palco pode estar com zoom)
32
+ function frame() { const r=root.querySelector('.slide').getBoundingClientRect(); return { r, k: r.width/1920, W: 1920, H: r.height/(r.width/1920) }; }
33
+ function boxOf(n, f = frame()) { const b=n.getBoundingClientRect(); return { l:(b.left-f.r.left)/f.k, t:(b.top-f.r.top)/f.k, w:b.width/f.k, h:b.height/f.k }; }
34
+ function shift(n, dx, dy) { const v=edit(n); if(dx) v.dx=Math.round((v.dx||0)+dx); if(dy) v.dy=Math.round((v.dy||0)+dy); }
35
+ // alinhar: vários objetos entre si (pela caixa da seleção); um só, no slide
36
+ function align(how) {
37
+ if(!sel.length)return;
38
+ const f=frame(), boxes=sel.map(n=>boxOf(n,f));
39
+ const T = sel.length>1 ? { l:Math.min(...boxes.map(b=>b.l)), t:Math.min(...boxes.map(b=>b.t)), r:Math.max(...boxes.map(b=>b.l+b.w)), b:Math.max(...boxes.map(b=>b.t+b.h)) } : { l:0, t:0, r:f.W, b:f.H };
40
+ sel.forEach((n,i)=>{ const b=boxes[i];
41
+ if(how==='left')shift(n,T.l-b.l,0); if(how==='right')shift(n,T.r-(b.l+b.w),0); if(how==='hcenter')shift(n,(T.l+T.r)/2-(b.l+b.w/2),0);
42
+ if(how==='top')shift(n,0,T.t-b.t); if(how==='bottom')shift(n,0,T.b-(b.t+b.h)); if(how==='vcenter')shift(n,0,(T.t+T.b)/2-(b.t+b.h/2)); });
43
+ finish();
44
+ }
45
+ // distribuir: o primeiro e o último ficam; os do meio, com o mesmo espaço entre um e outro
46
+ function distribute(axis) {
47
+ if(sel.length<3)return;
48
+ const f=frame(), items=sel.map(n=>({n,b:boxOf(n,f)})), H=axis==='h';
49
+ items.sort((a,c)=>H?a.b.l-c.b.l:a.b.t-c.b.t);
50
+ const start=H?items[0].b.l:items[0].b.t, end=H?items.at(-1).b.l+items.at(-1).b.w:items.at(-1).b.t+items.at(-1).b.h;
51
+ const total=items.reduce((a,x)=>a+(H?x.b.w:x.b.h),0), gap=(end-start-total)/(items.length-1);
52
+ let at=start;
53
+ items.forEach(({n,b})=>{ const cur=H?b.l:b.t; if(H)shift(n,at-cur,0);else shift(n,0,at-cur); at+=(H?b.w:b.h)+gap; });
54
+ finish();
55
+ }
56
+ const isImage = (n) => n.classList.contains('fig-img') || !!n.querySelector(':scope > img');
57
+ // trocar a imagem: vai para imagens/ do projeto (sem projeto, fica no deck) e entra no lugar da outra
58
+ let imgInput;
59
+ function replaceImage() {
60
+ const el=primary(); if(!el||!isImage(el))return;
61
+ if(!imgInput){imgInput=document.createElement('input');imgInput.type='file';imgInput.accept='image/png,image/jpeg,image/webp,image/gif';imgInput.hidden=true;document.body.append(imgInput);}
62
+ imgInput.onchange=async()=>{
63
+ const f=imgInput.files[0];imgInput.value='';if(!f)return;
64
+ if(f.size>8*1024*1024){alert('Use uma imagem de até 8 MB.');return;}
65
+ const dataUrl=await new Promise((ok,no)=>{const r=new FileReader();r.onload=()=>ok(r.result);r.onerror=no;r.readAsDataURL(f);});
66
+ let image=dataUrl;
67
+ try{const r=await fetch('api/project/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dir:'imagens',name:f.name,dataUrl})});const j=await r.json();if(r.ok&&j.path)image=j.path;}catch{}
68
+ pick([el]);change(e=>e.image=image);
69
+ };
70
+ imgInput.click();
71
+ }
72
+ function copyStyle() {
73
+ const el=primary(); if(!el)return;
74
+ const v=slide.visualEdits?.[el.dataset.vkey]||{}, cs=getComputedStyle(el);
75
+ // o que está na tela vale mesmo sem ajuste (tamanho e cor do tema): colar deixa igual ao que se vê
76
+ styleClip=Object.fromEntries(STYLE_KEYS.filter(k=>v[k]!=null).map(k=>[k,v[k]]));
77
+ if(el.classList.contains('t')){ styleClip.size ??= Math.round(parseFloat(cs.fontSize)); styleClip.color ??= toHex(cs.color); }
78
+ toolbar.querySelector('[data-act="paste-style"]').disabled=false;
79
+ }
80
+ function pasteStyle() {
81
+ if(!styleClip||!sel.length)return;
82
+ change(e=>{ STYLE_KEYS.forEach(k=>delete e[k]); Object.assign(e,styleClip); });
83
+ }
84
+ // guias: bordas e centro do slide e dos outros objetos; o arrasto gruda a menos de 8 px (Alt solta)
85
+ function guidesFor(f) {
86
+ const others=[...root.querySelectorAll('[data-vkey]')].filter(n=>!sel.includes(n)&&!sel.some(s=>s.contains(n)||n.contains(s))&&n.offsetParent&&getComputedStyle(n).display!=='none').map(n=>boxOf(n,f));
87
+ const xs=[0,f.W/2,f.W,...others.flatMap(b=>[b.l,b.l+b.w/2,b.l+b.w])], ys=[0,f.H/2,f.H,...others.flatMap(b=>[b.t,b.t+b.h/2,b.t+b.h])];
88
+ return {xs,ys};
89
+ }
90
+ function snap(d, dx, dy, alt) {
91
+ root.querySelectorAll('.visual-guide').forEach(g=>g.remove());
92
+ if(alt||!d.box)return [dx,dy];
93
+ const b=d.box, near=(vals,cands)=>{let best=null;for(const v of vals)for(const c of cands){const off=c-v;if(Math.abs(off)<=8&&(!best||Math.abs(off)<Math.abs(best.off)))best={off,at:c};}return best;};
94
+ const gx=near([b.l+dx,b.l+dx+b.w/2,b.l+dx+b.w],d.guides.xs), gy=near([b.t+dy,b.t+dy+b.h/2,b.t+dy+b.h],d.guides.ys);
95
+ const line=(vertical,at)=>{const g=document.createElement('div');g.className='visual-guide';const slideEl=root.querySelector('.slide');const off=slideEl.getBoundingClientRect(),rr=root.getBoundingClientRect(),k=(rr.width/root.offsetWidth)||1;
96
+ Object.assign(g.style,vertical?{left:`${(off.left-rr.left)/k+at*d.f.k/k}px`,top:`${(off.top-rr.top)/k}px`,width:'1px',height:`${off.height/k}px`}:{left:`${(off.left-rr.left)/k}px`,top:`${(off.top-rr.top)/k+at*d.f.k/k}px`,height:'1px',width:`${off.width/k}px`});root.append(g);};
97
+ if(gx){dx+=gx.off;line(true,gx.at);} if(gy){dy+=gy.off;line(false,gy.at);}
98
+ return [dx,dy];
99
+ }
100
+ // els: lista de elementos selecionados (vazia = nenhum). A alça de redimensionar só aparece com um único objeto.
101
+ function pick(els) {
102
+ els = (Array.isArray(els) ? els : els ? [els] : []).filter(Boolean);
103
+ root?.querySelectorAll('.visual-selected').forEach(n=>n.classList.remove('visual-selected'));
104
+ root?.querySelectorAll('.visual-handle').forEach(h=>h.remove());
105
+ sel=els; toolbar.hidden=!sel.length || !active;
106
+ if(!sel.length){notify();return;}
107
+ sel.forEach(el=>el.classList.add('visual-selected'));
108
+ const el=primary();
109
+ // só os controles que fazem sentido para o que está selecionado
110
+ const texts=sel.every(n=>n.classList.contains('t')), anyText=sel.some(n=>n.classList.contains('t')), shapes=sel.every(n=>n.classList.contains('shape'));
111
+ toolbar.querySelector('[data-size-field]').hidden=!texts;
112
+ toolbar.querySelector('[data-color-field]').hidden=!anyText;
113
+ toolbar.querySelector('[data-fill-field]').hidden=!shapes;
114
+ toolbar.querySelectorAll('[data-distribute]').forEach(b=>b.hidden=sel.length<3);
115
+ toolbar.querySelector('[data-act="replace-image"]').hidden=!(sel.length===1&&isImage(sel[0]));
116
+ toolbar.querySelector('[data-act="paste-style"]').disabled=!styleClip;
117
+ // o tamanho que a pessoa escolheu (e não o que o ajuste para caber deixou na tela)
118
+ toolbar.querySelector('[data-size]').value=Math.round(Number(slide.visualEdits?.[el.dataset.vkey]?.size)||parseFloat(getComputedStyle(el).fontSize));
119
+ toolbar.querySelector('[data-color]').value=toHex(getComputedStyle(el).color);
120
+ const fillOf=n=>n.querySelector('.shape-svg polygon, .shape-svg path')?getComputedStyle(n.querySelector('.shape-svg polygon, .shape-svg path')).fill:getComputedStyle(n).backgroundColor;
121
+ if(shapes)toolbar.querySelector('[data-fill]').value=toHex(fillOf(el));
122
+ if(sel.length===1) { const h=document.createElement('button');h.type='button';h.className='visual-handle';h.ariaLabel='Redimensionar elemento';el.append(h); }
123
+ placeToolbar();
124
+ trackToolbar();
125
+ notify();
126
+ }
127
+ const toHex=c=>{const m=String(c).match(/\d+(\.\d+)?/g);if(!m||m.length<3)return '#000000';return '#'+m.slice(0,3).map(v=>Math.round(+v).toString(16).padStart(2,'0')).join('');};
128
+ // a barra do objeto flutua logo acima da seleção (embaixo, se não couber), em qualquer aba da faixa
129
+ function placeToolbar() {
130
+ if(toolbar.hidden||!sel.length)return;
131
+ const rs=sel.map(n=>n.getBoundingClientRect()),top=Math.min(...rs.map(r=>r.top)),bottom=Math.max(...rs.map(r=>r.bottom)),left=Math.min(...rs.map(r=>r.left));
132
+ const h=toolbar.offsetHeight,w=toolbar.offsetWidth;
133
+ toolbar.style.top=`${Math.round(top-h-12<70?bottom+12:top-h-12)}px`;
134
+ toolbar.style.left=`${Math.round(Math.max(8,Math.min(innerWidth-w-8,left)))}px`;
135
+ }
136
+ let toolbarFrame;
137
+ function trackToolbar() {
138
+ cancelAnimationFrame(toolbarFrame);
139
+ const tick=()=>{if(toolbar.hidden||!sel.length)return;placeToolbar();toolbarFrame=requestAnimationFrame(tick);};
140
+ toolbarFrame=requestAnimationFrame(tick);
141
+ }
142
+ // cor ao vivo enquanto a pessoa escolhe; grava (com Desfazer) quando ela solta
143
+ function liveColor(kind,value) {
144
+ sel.forEach(n=>{if(kind==='color')n.style.setProperty('color',value,'important');else if(n.querySelector(':scope > .shape-svg'))n.style.setProperty('--shape-fill',value);else n.style.setProperty('background',value,'important');});
145
+ }
146
+ // escreve no texto: o cursor vai para onde a pessoa clicou; o app salva no blur (enableInlineEditing)
147
+ function startWriting(el,x,y) {
148
+ if(!el.classList.contains('t'))return;
149
+ pick(null);
150
+ el.contentEditable='true';el.dataset.writing='true';el.focus();
151
+ const r=document.caretRangeFromPoint?.(x,y);
152
+ if(r&&el.contains(r.startContainer)){const s=getSelection();s.removeAllRanges();s.addRange(r);}
153
+ el.addEventListener('blur',()=>{delete el.dataset.writing;if(active)el.contentEditable='false';},{once:true});
154
+ }
155
+ function insert(element) {
156
+ saveBefore();
157
+ const list=slide.layout==='canvas' ? 'elements' : 'add';
158
+ if(slide[list] && !Array.isArray(slide[list]))slide[list]=[slide[list]];
159
+ (slide[list] ||= []).push(element); active=true;finish();
160
+ }
161
+ function setMode(value) { active=value;root?.classList.toggle('visual-mode',active);if(!active){pick(null);root?.querySelectorAll('[contenteditable="false"]').forEach(n=>n.contentEditable='true');} } // saiu do ajuste: o texto volta a ser editável
162
+ function setup(container) {
163
+ host=container;
164
+ container.append(button('Texto',()=>insert({text:'Seu texto',x:180,y:200,w:650,h:100,size:64}),'rbtn rbtn-lg','type'));
165
+ // Formas: menu com as 12 formas desenhadas (abre abaixo do botão, fora do recorte da faixa)
166
+ const menu=document.createElement('div');menu.className='shape-menu';menu.hidden=true;menu.setAttribute('role','menu');menu.ariaLabel='Formas';
167
+ SHAPES.forEach(([k,label,svg])=>{const b=document.createElement('button');b.type='button';b.className='shape-option';b.dataset.shape=k;b.title=label;b.setAttribute('role','menuitem');b.innerHTML=`<svg viewBox="0 0 100 100" aria-hidden="true">${svg}</svg><span></span>`;b.querySelector('span').textContent=label;
168
+ b.onclick=()=>{menu.hidden=true;insert({shape:k,x:300,y:260,...shapeSize(k),fill:k==='line'?undefined:'hi',color:k==='line'?'hi':undefined});};menu.append(b);});
169
+ const shapesBtn=button('Formas',()=>{if(!menu.hidden){menu.hidden=true;return;}const r=shapesBtn.getBoundingClientRect();menu.style.left=`${Math.round(r.left)}px`;menu.style.top=`${Math.round(r.bottom+4)}px`;menu.hidden=false;menu.querySelector('button')?.focus();},'rbtn rbtn-lg','square');
170
+ shapesBtn.id='btn-insert-shape';shapesBtn.setAttribute('aria-haspopup','menu');container.append(shapesBtn);document.body.append(menu);
171
+ document.addEventListener('pointerdown',e=>{if(!menu.hidden&&!menu.contains(e.target)&&!shapesBtn.contains(e.target))menu.hidden=true;});
172
+ menu.addEventListener('keydown',e=>{if(e.key==='Escape'){menu.hidden=true;shapesBtn.focus();}});
173
+ const file=document.createElement('input');file.type='file';file.accept='image/png,image/jpeg,image/webp';file.hidden=true;container.append(file);
174
+ container.append(button('Imagem',()=>file.click(),'rbtn rbtn-lg','image'));
175
+ container.append(button('Tabela',()=>insert({table:{head:['Coluna 1','Coluna 2','Coluna 3'],rows:[['Item','10','20'],['Item','30','40']]},x:260,y:220,w:1100}),'rbtn rbtn-lg','table'));
176
+ file.onchange=async()=>{ const f=file.files[0];if(!f)return;if(f.size>8*1024*1024){alert('Use uma imagem de até 8 MB.');return;} const data=await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsDataURL(f);});insert({image:data,x:260,y:180,w:700,h:450,fit:'contain'});file.value=''; };
177
+ toolbar=document.createElement('div');toolbar.className='visual-toolbar';toolbar.hidden=true;toolbar.setAttribute('role','toolbar');toolbar.ariaLabel='Objeto selecionado';
178
+ toolbar.innerHTML=`<label class="vt-field" data-size-field title="Tamanho do texto"><i class="ic" data-ic="type"></i><input type="number" min="10" max="500" data-size aria-label="Tamanho do texto"></label>
179
+ <label class="vt-field vt-color" data-color-field title="Cor do texto"><i class="ic" data-ic="baseline"></i><input type="color" data-color aria-label="Cor do texto"></label>
180
+ <label class="vt-field vt-color" data-fill-field title="Preenchimento"><i class="ic" data-ic="paint-bucket"></i><input type="color" data-fill aria-label="Preenchimento"></label>
181
+ <span class="vt-sep"></span>
182
+ <span class="vt-group" data-align-group>
183
+ <button type="button" class="vt-btn" data-align="left" aria-label="Alinhar à esquerda" title="Alinhar à esquerda (um objeto: no slide)"><i class="ic" data-ic="align-start-vertical"></i></button>
184
+ <button type="button" class="vt-btn" data-align="hcenter" aria-label="Centralizar na horizontal" title="Centralizar na horizontal (um objeto: no slide)"><i class="ic" data-ic="align-center-vertical"></i></button>
185
+ <button type="button" class="vt-btn" data-align="right" aria-label="Alinhar à direita" title="Alinhar à direita (um objeto: no slide)"><i class="ic" data-ic="align-end-vertical"></i></button>
186
+ <button type="button" class="vt-btn" data-align="top" aria-label="Alinhar em cima" title="Alinhar em cima (um objeto: no slide)"><i class="ic" data-ic="align-start-horizontal"></i></button>
187
+ <button type="button" class="vt-btn" data-align="vcenter" aria-label="Centralizar na vertical" title="Centralizar na vertical (um objeto: no slide)"><i class="ic" data-ic="align-center-horizontal"></i></button>
188
+ <button type="button" class="vt-btn" data-align="bottom" aria-label="Alinhar embaixo" title="Alinhar embaixo (um objeto: no slide)"><i class="ic" data-ic="align-end-horizontal"></i></button>
189
+ <button type="button" class="vt-btn" data-distribute="h" aria-label="Distribuir na horizontal" title="Distribuir na horizontal (mesmo espaço entre 3 ou mais)"><i class="ic" data-ic="align-horizontal-distribute-center"></i></button>
190
+ <button type="button" class="vt-btn" data-distribute="v" aria-label="Distribuir na vertical" title="Distribuir na vertical (mesmo espaço entre 3 ou mais)"><i class="ic" data-ic="align-vertical-distribute-center"></i></button>
191
+ </span>
192
+ <span class="vt-sep"></span>
193
+ <button type="button" class="vt-btn" data-act="replace-image" aria-label="Trocar imagem" title="Trocar imagem (fica no mesmo lugar, com os mesmos ajustes)"><i class="ic" data-ic="image-plus"></i></button>
194
+ <button type="button" class="vt-btn" data-act="copy-style" aria-label="Copiar estilo" title="Copiar estilo (Ctrl+Alt+C)"><i class="ic" data-ic="paintbrush"></i></button>
195
+ <button type="button" class="vt-btn" data-act="paste-style" aria-label="Colar estilo" title="Colar estilo (Ctrl+Alt+V)" disabled><i class="ic" data-ic="clipboard-paste"></i></button>
196
+ <button type="button" class="vt-btn" data-act="lock" aria-label="Travar" title="Travar: o clique atravessa (destrave na lista de objetos do Formatar)"><i class="ic" data-ic="lock"></i></button>
197
+ <span class="vt-sep"></span>
198
+ <button type="button" class="vt-btn" data-act="front" aria-label="Trazer para frente" title="Trazer para frente"><i class="ic" data-ic="bring-to-front"></i></button>
199
+ <button type="button" class="vt-btn" data-act="back" aria-label="Enviar para trás" title="Enviar para trás (fica atrás dos outros objetos, nunca do fundo)"><i class="ic" data-ic="send-to-back"></i></button>
200
+ <button type="button" class="vt-btn" data-act="delete" aria-label="Excluir" title="Excluir (Delete)"><i class="ic" data-ic="trash-2"></i></button>`;
201
+ const size=toolbar.querySelector('[data-size]'),color=toolbar.querySelector('[data-color]'),fill=toolbar.querySelector('[data-fill]');
202
+ size.onchange=()=>change(e=>e.size=Math.max(10,Math.min(500,Number(size.value))));
203
+ color.oninput=()=>liveColor('color',color.value);color.onchange=()=>change(e=>e.color=color.value);
204
+ fill.oninput=()=>liveColor('fill',fill.value);fill.onchange=()=>change(e=>e.fill=fill.value);
205
+ toolbar.querySelector('[data-act="front"]').onclick=()=>change(e=>e.z=(e.z||0)+1);
206
+ toolbar.querySelector('[data-act="back"]').onclick=()=>change(e=>e.z=(e.z||0)-1);
207
+ toolbar.querySelector('[data-act="delete"]').onclick=()=>{change(e=>e.hidden=true);pick(null);};
208
+ toolbar.querySelectorAll('[data-align]').forEach(b=>b.onclick=()=>align(b.dataset.align));
209
+ toolbar.querySelectorAll('[data-distribute]').forEach(b=>b.onclick=()=>distribute(b.dataset.distribute));
210
+ toolbar.querySelector('[data-act="copy-style"]').onclick=copyStyle;
211
+ toolbar.querySelector('[data-act="replace-image"]').onclick=()=>replaceImage();
212
+ toolbar.querySelector('[data-act="paste-style"]').onclick=pasteStyle;
213
+ toolbar.querySelector('[data-act="lock"]').onclick=()=>{change(e=>e.locked=true);pick(null);};
214
+ toolbar.addEventListener('pointerdown',e=>e.stopPropagation());
215
+ document.body.append(toolbar);
216
+ addEventListener('resize',placeToolbar);document.addEventListener('scroll',placeToolbar,true);
217
+ document.addEventListener('keydown',e=>{
218
+ if(!active || e.target.closest('input,textarea,[contenteditable="true"],dialog') || document.querySelector('dialog[open]'))return;
219
+ // Ctrl+Z / Ctrl+Y: o desfazer único do Studio (history.js), o mesmo do resto da tela
220
+ if(!sel.length)return;
221
+ if((e.ctrlKey||e.metaKey)&&e.altKey&&['c','v'].includes(e.key.toLowerCase())){e.preventDefault();e.stopImmediatePropagation();e.key.toLowerCase()==='c'?copyStyle():pasteStyle();return;}
222
+ if(['Delete','Backspace'].includes(e.key)){e.preventDefault();e.stopImmediatePropagation();change(v=>v.hidden=true);pick(null);return;}
223
+ if(e.key==='Escape'){pick(null);setMode(false);return;}
224
+ // Tab / Shift+Tab: próximo objeto (alcança o que ficou escondido atrás de outro)
225
+ if(e.key==='Tab'){const all=[...root.querySelectorAll('[data-vkey]')].filter(n=>n.offsetParent&&getComputedStyle(n).display!=='none');if(!all.length)return;e.preventDefault();e.stopImmediatePropagation();const i=all.indexOf(primary());pick(all[(i+(e.shiftKey?-1:1)+all.length)%all.length]);return;}
226
+ if(e.key.startsWith('Arrow')){e.preventDefault();e.stopImmediatePropagation();const n=e.shiftKey?10:1;change(v=>{v.dx=(v.dx||0)+(e.key==='ArrowRight'?n:e.key==='ArrowLeft'?-n:0);v.dy=(v.dy||0)+(e.key==='ArrowDown'?n:e.key==='ArrowUp'?-n:0);});}
227
+ },true);
228
+ }
229
+ function enterMode() {
230
+ if(!active)setMode(true);
231
+ root.querySelectorAll('[contenteditable]').forEach(n=>{n.contentEditable='false';delete n.dataset.writing;});document.activeElement?.blur();
232
+ }
233
+ function startDrag(x,y,pointerId,resize) {
234
+ const scale=root.querySelector('.slide').getBoundingClientRect().width/1920;const el=primary();const r=el.getBoundingClientRect();
235
+ beforeDrag=clone(slide);
236
+ const f=frame(),boxes=sel.map(n=>boxOf(n,f));
237
+ const box={l:Math.min(...boxes.map(b=>b.l)),t:Math.min(...boxes.map(b=>b.t))};box.w=Math.max(...boxes.map(b=>b.l+b.w))-box.l;box.h=Math.max(...boxes.map(b=>b.t+b.h))-box.t;
238
+ dragging={x,y,scale,resize,w:r.width/scale,h:r.height/scale,moved:false,f,box,guides:resize?null:guidesFor(f),items:sel.filter(n=>!locked(n)).map(n=>({el:n,value:clone(slide.visualEdits?.[n.dataset.vkey]||{})}))};
239
+ try{root.setPointerCapture(pointerId);}catch{}
240
+ }
241
+ // o objeto mais de cima naquele ponto, atravessando as caixas transparentes do layout: um objeto enviado para trás
242
+ // do texto continua clicável onde ele aparece
243
+ function objectAt(x,y) {
244
+ for(const n of document.elementsFromPoint(x,y)){if(!root.contains(n))continue;const o=n.closest('[data-vkey]');if(o&&root.contains(o)&&!locked(o)&&getComputedStyle(o).display!=='none')return o;}
245
+ return null;
246
+ }
247
+ // objetos inteiros dentro do retângulo (o de fora ganha do de dentro: um cartão e não o texto dele)
248
+ function inMarquee(box) {
249
+ const all=[...root.querySelectorAll('[data-vkey]')].filter(n=>n.offsetParent&&!locked(n)&&getComputedStyle(n).display!=='none');
250
+ const hit=all.filter(n=>{const r=n.getBoundingClientRect();return r.width&&r.left>=box.left&&r.right<=box.right&&r.top>=box.top&&r.bottom<=box.bottom;});
251
+ return hit.filter(n=>!hit.some(o=>o!==n&&o.contains(n)));
252
+ }
253
+ function mount(container,s,onCommit) {
254
+ if(slide!==s){sel=[];}
255
+ const keys=sel.map(n=>n.dataset.vkey);
256
+ root=container;slide=s;commit=onCommit;root.classList.toggle('visual-mode',active);
257
+ if(!root.dataset.visualEvents){
258
+ root.dataset.visualEvents='true';
259
+ root.addEventListener('pointerdown',e=>{
260
+ if(e.button!==0 || e.target.closest('.science-plot,input,textarea'))return;
261
+ if(e.target.closest('[contenteditable="true"]')?.dataset.writing)return; // escrevendo: o clique é do cursor
262
+ const el=objectAt(e.clientX,e.clientY)||(locked(e.target.closest('[data-vkey]'))?null:e.target.closest('[data-vkey]'));
263
+ if(!el){
264
+ if(!e.target.closest('.slide')){if(active){pick(null);setMode(false);}return;}
265
+ // área vazia do slide: retângulo de seleção
266
+ e.preventDefault();e.stopImmediatePropagation();document.activeElement?.blur();
267
+ const rr=root.getBoundingClientRect();const box=document.createElement('div');box.className='visual-marquee';root.append(box);
268
+ marquee={x:e.clientX,y:e.clientY,rr,k:(rr.width/root.offsetWidth)||1,box,add:e.shiftKey,before:e.shiftKey?[...sel]:[]}; // k: o palco pode estar com zoom
269
+ try{root.setPointerCapture(e.pointerId);}catch{}
270
+ return;
271
+ }
272
+ e.preventDefault();e.stopImmediatePropagation();
273
+ const resize=e.target.classList.contains('visual-handle');
274
+ // clicar de novo no texto já selecionado: escreve (se não arrastar)
275
+ if(!resize&&sel.length===1&&sel[0]===el&&el.classList.contains('t')&&!e.shiftKey){pendingWrite={el};startDrag(e.clientX,e.clientY,e.pointerId,false);return;}
276
+ enterMode();
277
+ if(e.shiftKey){pick(sel.includes(el)?sel.filter(n=>n!==el):[...sel,el]);if(!sel.length)return;}
278
+ else if(!sel.includes(el))pick(el);
279
+ else if(sel.length>1&&!resize)pendingSolo=el; // clique sem arrastar num dos selecionados: fica só ele
280
+ startDrag(e.clientX,e.clientY,e.pointerId,resize);
281
+ },true);
282
+ root.addEventListener('pointermove',e=>{
283
+ if(marquee){const m=marquee,l=Math.min(m.x,e.clientX),t=Math.min(m.y,e.clientY);Object.assign(m.box.style,{left:`${(l-m.rr.left)/m.k}px`,top:`${(t-m.rr.top)/m.k}px`,width:`${Math.abs(e.clientX-m.x)/m.k}px`,height:`${Math.abs(e.clientY-m.y)/m.k}px`});m.moved=Math.hypot(e.clientX-m.x,e.clientY-m.y)>4;return;}
284
+ if(!dragging)return;const d=dragging;let dx=(e.clientX-d.x)/d.scale,dy=(e.clientY-d.y)/d.scale;
285
+ if(!d.resize&&d.moved)[dx,dy]=snap(d,dx,dy,e.altKey);
286
+ if(!d.moved&&Math.hypot(e.clientX-d.x,e.clientY-d.y)<4)return;
287
+ if(!d.moved){d.moved=true;if(pendingWrite){pendingWrite=null;enterMode();}}
288
+ if(d.resize){const el=primary(),v=edit(el);v.w=Math.max(30,Math.round(d.w+dx));v.h=Math.max(30,Math.round(d.h+dy));el.style.setProperty('width',v.w+'px','important');el.style.setProperty('height',v.h+'px','important');}
289
+ else d.items.forEach(({el,value})=>{const v=edit(el);v.dx=Math.round((value.dx||0)+dx);v.dy=Math.round((value.dy||0)+dy);el.style.translate=`${v.dx}px ${v.dy}px`;});});
290
+ const up=e=>{
291
+ if(marquee){const m=marquee;marquee=null;const b=m.box.getBoundingClientRect();m.box.remove();
292
+ if(!m.moved){if(!m.add){pick(null);setMode(false);}return;}
293
+ const hit=inMarquee(b);enterMode();pick(m.add?[...new Set([...m.before,...hit])]:hit);if(!sel.length)setMode(false);return;}
294
+ const w=pendingWrite;pendingWrite=null;const solo=pendingSolo;pendingSolo=null;
295
+ root.querySelectorAll('.visual-guide').forEach(g=>g.remove());
296
+ if(!dragging)return;const d=dragging;dragging=null;
297
+ if(w&&!d.moved){startWriting(w.el,e.clientX,e.clientY);return;}
298
+ if(solo&&!d.moved){pick([solo]);return;}
299
+ if(JSON.stringify(beforeDrag)!==JSON.stringify(slide)){finish();}
300
+ };
301
+ root.addEventListener('pointerup',up);root.addEventListener('pointercancel',up);
302
+ root.addEventListener('dblclick',e=>{const el=e.target.closest('.t[data-vkey]');if(!el||el.dataset.writing)return;startWriting(el,e.clientX,e.clientY);});
303
+ }
304
+ if(active)root.querySelectorAll('[contenteditable]').forEach(n=>n.contentEditable='false');
305
+ restoring=true; try { pick(keys.map(k=>root.querySelector(`[data-vkey="${CSS.escape(k)}"]`))); } finally { restoring=false; }
306
+ }
307
+ // Inspetor: lê a seleção e grava uma propriedade em todos os selecionados (com Desfazer); null apaga a propriedade
308
+ function setProp(key, value) {
309
+ change((e) => { if (value === null || value === undefined || value === '') delete e[key]; else e[key] = value; });
310
+ }
311
+ function clearEdits() { if(!sel.length)return;saveBefore();sel.forEach((n)=>{ if(slide.visualEdits) delete slide.visualEdits[n.dataset.vkey]; });finish(); }
312
+ function setPropOn(els, key, value) {
313
+ if(!els?.length||!slide)return;
314
+ els.forEach(n=>{ const e=edit(n); if(value===null||value===undefined||value==='') delete e[key]; else e[key]=value; });
315
+ finish();
316
+ }
317
+ window.SagaVisual={setup,mount,
318
+ selection:()=>sel.slice(),
319
+ objects:()=>root?[...root.querySelectorAll('.slide [data-vkey]')]:[],
320
+ replaceImage,
321
+ setPropOn,
322
+ edits:(el)=>({...(slide?.visualEdits?.[el.dataset.vkey]||{})}),
323
+ setProp, clearEdits,
324
+ onSelect:(fn)=>listeners.push(fn),
325
+ select:(els)=>{ if(!root)return; if(els?.length && !active)setMode(true); pick(els); }};
326
+ })();
@@ -0,0 +1,18 @@
1
+ window.SagaWorkspace = function () {
2
+ // Cada editor tem sua própria apresentação aberta, mesmo no Studio local.
3
+ const workspaceId = crypto.randomUUID();
4
+ const scopedURL = value => { const u = new URL(value, location.href); if (u.origin === location.origin) u.searchParams.set('_workspace', workspaceId); return u.href; };
5
+ const originalFetch = window.fetch.bind(window);
6
+ window.fetch = (input, options) => {
7
+ const url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url, location.href);
8
+ if (url.origin !== location.origin) return originalFetch(input, options);
9
+ const headers = new Headers(options?.headers || input?.headers);
10
+ headers.set('x-sagadeck-workspace', workspaceId);
11
+ return originalFetch(input, { ...options, headers }).catch(error=>{
12
+ if(error.name==='AbortError')throw error;
13
+ if(error instanceof TypeError)throw new Error('Não foi possível conectar ao SagaDeck. Verifique a conexão e tente novamente. Código: CONNECTION_FAILED. Se persistir, informe esse código ao responsável pelo SagaDeck.',{cause:error});
14
+ throw error;
15
+ });
16
+ };
17
+ return scopedURL;
18
+ };
@@ -0,0 +1 @@
1
+ export const PUBLIC_SCRIPTS = new Set(["/workspace.js", "/chat-progress.js", "/slide-clipboard.js", "/app.js", "/ui-icons.js", "/slide-form.js", "/lab-fields.js", "/calc-fields.js", "/art-preview.js", "/library.js", "/screenshot-editor.js", "/visual-editor.js", "/inspector.js", "/explorer.js", "/viewers.js", "/merge-decks.js", "/history.js", "/review-ui.js", "/share-ui.js", "/developer-fields.js", "/api-collections.js", "/studio-awareness.js"]);
@@ -0,0 +1,41 @@
1
+ // "Olhos" da IA: foto do slide renderizado (uma por clique se o pedido falar de animação/ordem).
2
+ // Sem Chrome disponível, segue sem foto — a IA só perde a visão, o pedido continua.
3
+ const ANIM_WORDS = /clique|click|anima|aparec|revel|ordem|sequ[eê]n|entra|some|surge|transi/i;
4
+ export async function lookAt(spec, index, prompt, emit) {
5
+ if (typeof index !== "number" || !spec?.slides?.[index]) return [];
6
+ try {
7
+ emit({ phase: "looking", text: "Olhando o slide…" });
8
+ const { slideSnapshots } = await import("./snapshot.js");
9
+ return await slideSnapshots(spec, index, { mode: ANIM_WORDS.test(prompt) ? "steps" : "final" });
10
+ } catch (e) {
11
+ console.warn("[Studio] sem foto do slide para a IA:", e.message);
12
+ return [];
13
+ }
14
+ }
15
+
16
+ // Resposta de uma tarefa de IA. Com stream, manda NDJSON: uma linha {type:"progress",…} por etapa/pedaço
17
+ // de texto e, no fim, {type:"result", data} ou {type:"error", error}. Sem stream, um JSON só.
18
+
19
+ export function readBody(req) {
20
+ return new Promise((resolve, reject) => {
21
+ const chunks = [];
22
+ req.on("data", (c) => chunks.push(c));
23
+ req.on("end", () => resolve(Buffer.concat(chunks)));
24
+ req.on("error", reject);
25
+ });
26
+ }
27
+
28
+ export function readJSON(req) {
29
+ return new Promise((resolve, reject) => {
30
+ let data = "";
31
+ req.on("data", (chunk) => (data += chunk));
32
+ req.on("end", () => {
33
+ try {
34
+ resolve(data ? JSON.parse(data) : {});
35
+ } catch (e) {
36
+ reject(new Error("JSON inválido no corpo da requisição"));
37
+ }
38
+ });
39
+ req.on("error", reject);
40
+ });
41
+ }