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.
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- 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
|
+
}
|