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,155 @@
1
+ // Junção a três do deck, para nada se perder quando a pessoa mexe enquanto a IA pensa.
2
+ // base = o deck quando o pedido foi para a IA · meu = o que a pessoa tem agora · ia = o que a IA devolveu
3
+ // Regras: o que só a IA mudou entra; o que só a pessoa mudou fica; se os dois mudaram a MESMA coisa, vale a da
4
+ // pessoa (e o conflito é avisado). Com uid (identidade persistente do slide), os slides são casados pela identidade e
5
+ // a junção é campo a campo: a IA muda o título, a pessoa move uma imagem do mesmo slide, as duas coisas ficam; os
6
+ // ajustes visuais (visualEdits) juntam objeto a objeto. Sem uid (deck antigo), casa pelo conteúdo, como antes.
7
+ // Serve ao Studio (navegador) e ao servidor (Node): globalThis.SagadeckMerge.
8
+ (function (root) {
9
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
10
+ const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
11
+
12
+ // casamento monotônico (LCS) entre duas listas de slides iguais: devolve Map(índice em a -> índice em b)
13
+ function match(a, b) {
14
+ const n = a.length, m = b.length, ka = a.map((x) => JSON.stringify(x)), kb = b.map((x) => JSON.stringify(x));
15
+ const L = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
16
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = ka[i] === kb[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
17
+ const map = new Map();
18
+ for (let i = 0, j = 0; i < n && j < m;) {
19
+ if (ka[i] === kb[j]) { map.set(i, j); i++; j++; } else if (L[i + 1][j] >= L[i][j + 1]) i++; else j++;
20
+ }
21
+ return map;
22
+ }
23
+
24
+ // campo a campo; objetos de ajuste (visualEdits e cada objeto dentro) juntam por chave
25
+ const DEEP = new Set(["visualEdits"]);
26
+ function mergeFields(b, m, a, deep, conflict) {
27
+ const out = {};
28
+ for (const k of new Set([...Object.keys(b || {}), ...Object.keys(m || {}), ...Object.keys(a || {})])) {
29
+ const bv = b?.[k], mv = m?.[k], av = a?.[k];
30
+ let v;
31
+ if (eq(bv, mv)) v = av;
32
+ else if (eq(bv, av) || eq(mv, av)) v = mv;
33
+ else if (deep && isObj(bv) && isObj(mv) && isObj(av)) v = mergeFields(bv, mv, av, deep > 1 ? deep - 1 : 0, conflict);
34
+ else if (!deep && DEEP.has(k) && isObj(mv) && isObj(av)) v = mergeFields(isObj(bv) ? bv : {}, mv, av, 2, conflict);
35
+ else { v = mv; conflict(); }
36
+ if (v !== undefined) out[k] = v;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ // um lado sem uid (deck recém-gerado, página que ainda não recarregou): o slide herda o uid do mesmo slide do
42
+ // outro lado (casado pelo conteúdo; listas do mesmo tamanho, pela posição). Sem isso, o uid parece uma mudança
43
+ // da pessoa e a mudança da IA se perde.
44
+ const strip = (s) => { if (!isObj(s) || !("uid" in s)) return s; const { uid, ...r } = s; return r; };
45
+ function adopt(target, source) {
46
+ if (!target.some((s) => isObj(s) && !s.uid) || !source.some((s) => s?.uid)) return target;
47
+ const map = match(target.map(strip), source.map(strip));
48
+ const used = new Set(target.map((s) => s?.uid).filter(Boolean));
49
+ const taken = new Set(map.values());
50
+ return target.map((s, i) => {
51
+ if (!isObj(s) || s.uid) return s;
52
+ const j = map.has(i) ? map.get(i) : target.length === source.length && !taken.has(i) ? i : -1;
53
+ const u = j >= 0 ? source[j]?.uid : null;
54
+ if (!u || used.has(u)) return s;
55
+ used.add(u);
56
+ return { ...s, uid: u };
57
+ });
58
+ }
59
+
60
+ function mergeByUid(bs, ms, as, conflicts) {
61
+ const B = new Map(bs.map((s, i) => [s.uid, { s, i }])), M = new Map(ms.map((s) => [s.uid, s])), A = new Map(as.filter((s) => s?.uid).map((s) => [s.uid, s]));
62
+ const merged = (uid) => {
63
+ const b = B.get(uid);
64
+ let hit = false;
65
+ const s = mergeFields(b.s, M.get(uid), A.get(uid), 0, () => { hit = true; });
66
+ if (hit) conflicts.push(b.i);
67
+ return s;
68
+ };
69
+ const inBoth = (u) => B.has(u);
70
+ const userOrder = ms.map((s) => s.uid).filter(inBoth), baseOrder = bs.map((s) => s.uid).filter((u) => M.has(u));
71
+ const userReordered = !eq(userOrder, baseOrder);
72
+ const out = [];
73
+ const placed = new Set();
74
+ const push = (s) => { out.push(s); if (s?.uid) placed.add(s.uid); };
75
+ if (!userReordered) {
76
+ // a ordem é a da IA (ela pode ter inserido, apagado e reordenado)
77
+ for (const s of as) {
78
+ if (!s?.uid || !B.has(s.uid)) { push(s); continue; } // slide novo da IA
79
+ if (!M.has(s.uid)) continue; // a pessoa apagou
80
+ push(merged(s.uid));
81
+ }
82
+ // a IA apagou um slide que a pessoa mudou: fica o da pessoa (conflito), no lugar em que estava
83
+ bs.forEach((b, i) => {
84
+ if (A.has(b.uid) || !M.has(b.uid) || eq(b, M.get(b.uid))) return;
85
+ const prev = bs.slice(0, i).reverse().find((x) => placed.has(x.uid));
86
+ const at = prev ? out.findIndex((x) => x.uid === prev.uid) + 1 : 0;
87
+ out.splice(at, 0, M.get(b.uid)); placed.add(b.uid); conflicts.push(i);
88
+ });
89
+ } else {
90
+ // a pessoa reorganizou: vale a ordem dela, com o conteúdo junto campo a campo
91
+ for (const s of ms) {
92
+ if (!B.has(s.uid)) { push(s); continue; } // slide novo da pessoa
93
+ if (!A.has(s.uid)) { if (!eq(B.get(s.uid).s, s)) { push(s); conflicts.push(B.get(s.uid).i); } continue; } // a IA apagou
94
+ push(merged(s.uid));
95
+ }
96
+ // slides novos da IA: depois do mesmo vizinho em que ela pôs
97
+ as.forEach((s, j) => {
98
+ if (s?.uid && B.has(s.uid)) return;
99
+ const prev = as.slice(0, j).reverse().find((x) => x?.uid && placed.has(x.uid));
100
+ const at = prev ? out.findIndex((x) => x.uid === prev.uid) + 1 : out.length;
101
+ out.splice(at, 0, s); if (s?.uid) placed.add(s.uid);
102
+ });
103
+ }
104
+ // slides novos da pessoa (quando a ordem é a da IA): depois do vizinho de antes
105
+ if (!userReordered) ms.forEach((s, j) => {
106
+ if (B.has(s.uid) || placed.has(s.uid)) return;
107
+ const prev = ms.slice(0, j).reverse().find((x) => placed.has(x.uid));
108
+ const at = prev ? out.findIndex((x) => x.uid === prev.uid) + 1 : 0;
109
+ out.splice(at, 0, s); placed.add(s.uid);
110
+ });
111
+ return out;
112
+ }
113
+
114
+ function mergeDecks(base, mine, ai) {
115
+ if (eq(base, mine)) return { deck: ai, conflicts: [], kept: false };
116
+ const deck = {};
117
+ const keys = new Set([...Object.keys(base || {}), ...Object.keys(mine || {}), ...Object.keys(ai || {})]);
118
+ keys.delete("slides");
119
+ for (const k of keys) {
120
+ const v = k.startsWith("_") || !eq(base[k], mine[k]) ? mine[k] : ai[k];
121
+ if (v !== undefined) deck[k] = v;
122
+ }
123
+ let bs = base.slides || [], ms = mine.slides || [], as = ai.slides || [];
124
+ bs = adopt(bs, ms); ms = adopt(ms, bs); bs = adopt(bs, as); as = adopt(as, bs);
125
+ const conflicts = [];
126
+ let kept = false;
127
+ const withUid = (list) => list.every((s) => s && typeof s.uid === "string" && s.uid);
128
+ if (eq(bs, ms)) deck.slides = as; // a pessoa não mexeu nos slides
129
+ else if (eq(bs, as)) deck.slides = ms; // a IA não mexeu nos slides
130
+ else if (withUid(bs) && withUid(ms)) deck.slides = mergeByUid(bs, ms, as, conflicts);
131
+ else if (ms.length === bs.length) { // a pessoa editou slides no lugar
132
+ if (as.length === bs.length) {
133
+ deck.slides = bs.map((b, i) => {
134
+ const userChanged = !eq(b, ms[i]), aiChanged = !eq(b, as[i]);
135
+ if (userChanged && aiChanged && !eq(ms[i], as[i])) conflicts.push(i);
136
+ return userChanged ? ms[i] : as[i];
137
+ });
138
+ } else { // a IA inseriu/removeu: casa pelo conteúdo
139
+ const map = match(bs, as);
140
+ deck.slides = as.slice();
141
+ bs.forEach((b, i) => {
142
+ if (eq(b, ms[i])) return;
143
+ if (map.has(i)) deck.slides[map.get(i)] = ms[i];
144
+ else conflicts.push(i); // a IA mudou ou apagou um slide que a pessoa também mudou
145
+ });
146
+ }
147
+ } else { // a pessoa reorganizou os slides enquanto isso: fica a dela
148
+ deck.slides = ms;
149
+ kept = true;
150
+ }
151
+ return { deck, conflicts, kept };
152
+ }
153
+
154
+ root.SagadeckMerge = { mergeDecks };
155
+ })(typeof globalThis !== "undefined" ? globalThis : window);
@@ -0,0 +1,100 @@
1
+ // Revisão das mudanças (review: { status, note, original, pair }): o selo na miniatura, a faixa no slide (Ver original,
2
+ // Aceitar, Desfazer) e a lista Revisar › Mudanças. Saiu de app.js; o app passa o que ela usa (create(ctx)).
3
+ (function () {
4
+ function create(ctx) {
5
+ const { state, showToast, renderThumbnails, renderCurrentSlide, trackDeck, hydrateIcons, plainTitle } = ctx;
6
+ // ------------------------------------------------------------------ revisão (review: { status, note, original })
7
+ const reviewImage = (slide) => {
8
+ const n = slide?.review?.original;
9
+ if (slide?.review?.image) return slide.review.image;
10
+ return n ? `original/slide-${String(n).padStart(2, "0")}.png` : null;
11
+ };
12
+ const reviewCount = () => (state.deck?.slides || []).filter((s) => s.review?.status).length;
13
+ // marca de revisão: novo, alterado, pendente (proposta ao lado do original), revisar (conferir o desenho)
14
+ const REVIEW_KIND = {
15
+ novo: { cls: "new", icon: "plus", tag: "novo", title: "Slide novo", reject: "Tira este slide" },
16
+ alterado: { cls: "changed", icon: "pencil", tag: "alterado", title: "Alterado", reject: "Volta este slide ao original" },
17
+ pendente: { cls: "pending", icon: "git-compare", tag: "pendente", title: "Proposta pendente", reject: "Tira a proposta (o original, logo antes, fica)", accept: "Fica a proposta; o original ao lado sai" },
18
+ revisar: { cls: "check", icon: "scan-eye", tag: "revisar", title: "Conferir o desenho", reject: "Volta este slide ao original" },
19
+ };
20
+ const reviewKind = (rv) => REVIEW_KIND[rv?.status] || REVIEW_KIND.alterado;
21
+ function syncReviewBadge() {
22
+ const b = document.getElementById("review-badge");
23
+ if (!b) return;
24
+ const n = reviewCount();
25
+ b.textContent = n; b.classList.toggle("clean", !n);
26
+ }
27
+ function renderReviewBar(slide, idx) {
28
+ const bar = document.getElementById("review-bar"), orig = document.getElementById("review-original");
29
+ syncReviewBadge();
30
+ if (!bar) return;
31
+ const rv = slide?.review;
32
+ orig.classList.add("hidden");
33
+ if (!rv?.status) { bar.classList.add("hidden"); bar.innerHTML = ""; return; }
34
+ const img = reviewImage(slide);
35
+ const k = reviewKind(rv);
36
+ bar.className = `review-bar rv-${k.cls}`;
37
+ bar.innerHTML = `<i class="ic" data-ic="${k.icon}"></i><b>${k.title}${rv.status !== "novo" && rv.original ? ` (${rv.status === "pendente" ? "para o" : "era o"} ${rv.original})` : ""}</b><span class="rb-note"></span>
38
+ ${img ? '<button type="button" class="btn btn-sm" data-rv="orig" title="Mostra o slide original por cima (clique de novo para voltar)"><i class="ic" data-ic="eye"></i> Ver original</button>' : ""}
39
+ <button type="button" class="btn btn-sm" data-rv="reject" title="${k.reject}"><i class="ic" data-ic="rotate-ccw"></i> Desfazer</button>
40
+ <button type="button" class="btn btn-primary btn-sm" data-rv="accept" title="${k.accept || "Fica assim; a marca sai"}"><i class="ic" data-ic="check"></i> Aceitar</button>`;
41
+ bar.querySelector(".rb-note").textContent = rv.note || "";
42
+ hydrateIcons(bar);
43
+ // a foto do original só carrega quando a pessoa pede (e pode não existir: importado sem PowerPoint)
44
+ bar.querySelector('[data-rv="orig"]')?.addEventListener("click", async (e) => {
45
+ const btn = e.currentTarget, im = orig.querySelector("img");
46
+ if (!orig.classList.contains("hidden")) { orig.classList.add("hidden"); btn.classList.remove("active"); return; }
47
+ const url = `api/project/file?path=${encodeURIComponent(img)}`;
48
+ if (im.dataset.src !== url) {
49
+ const ok = await fetch(url, { method: "HEAD" }).then((r) => r.ok).catch(() => false);
50
+ if (!ok) return showToast("Não há foto deste slide do original (a importação foi feita sem PowerPoint ou LibreOffice).");
51
+ im.src = url; im.dataset.src = url;
52
+ }
53
+ orig.classList.remove("hidden"); btn.classList.add("active");
54
+ });
55
+ bar.querySelector('[data-rv="accept"]').onclick = () => reviewAction("accept", idx);
56
+ bar.querySelector('[data-rv="reject"]').onclick = () => reviewAction("reject", idx);
57
+ }
58
+ async function reviewAction(action, idx) {
59
+ try {
60
+ const r = await fetch("api/review", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, idx }) });
61
+ const j = await r.json();
62
+ if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`);
63
+ state.deck = j.spec;
64
+ trackDeck(action === "accept" ? "Aceitar mudança" : action === "acceptAll" ? "Aceitar todas as mudanças" : "Desfazer mudança da revisão");
65
+ if (state.currentSlideIndex >= state.deck.slides.length) state.currentSlideIndex = state.deck.slides.length - 1;
66
+ renderThumbnails(); await renderCurrentSlide();
67
+ document.getElementById("review-dialog")?.dispatchEvent(new Event("refresh"));
68
+ } catch (e) { showToast(e.message); }
69
+ }
70
+ function openReviewList() {
71
+ let dlg = document.getElementById("review-dialog");
72
+ const fill = () => {
73
+ const list = (state.deck?.slides || []).map((s, i) => ({ s, i })).filter(({ s }) => s.review?.status);
74
+ dlg.innerHTML = `<header><b>Mudanças para validar</b><span class="rv-count">${list.length}</span><span class="grow"></span>${list.length ? '<button type="button" class="btn btn-sm" data-all>Aceitar todas</button>' : ""}<button type="button" class="btn btn-sm" data-close title="Fechar"><i class="ic" data-ic="x"></i></button></header>
75
+ <div class="rv-list">${list.length ? list.map(({ s, i }) => `<div class="rv-item" data-i="${i}"><span class="thumb-review rv-${reviewKind(s.review).cls}">${reviewKind(s.review).tag}</span><b class="rv-num">${i + 1}</b><span class="rv-text"><span class="rv-title"></span><span class="rv-note"></span></span><button type="button" class="btn btn-sm" data-go>Ver</button><button type="button" class="btn btn-sm" data-reject>Desfazer</button><button type="button" class="btn btn-primary btn-sm" data-accept>Aceitar</button></div>`).join("") : '<div class="rv-empty">Nada para validar: todas as mudanças foram aceitas.</div>'}</div>`;
76
+ dlg.querySelectorAll(".rv-item").forEach((row) => {
77
+ const i = +row.dataset.i, s = state.deck.slides[i];
78
+ row.querySelector(".rv-title").textContent = plainTitle(s, i);
79
+ row.querySelector(".rv-note").textContent = s.review.note || "";
80
+ row.querySelector("[data-go]").onclick = () => { state.currentSlideIndex = i; renderThumbnails(); renderCurrentSlide(); };
81
+ row.querySelector("[data-accept]").onclick = () => reviewAction("accept", i);
82
+ row.querySelector("[data-reject]").onclick = () => reviewAction("reject", i);
83
+ });
84
+ dlg.querySelector("[data-all]")?.addEventListener("click", () => reviewAction("acceptAll"));
85
+ dlg.querySelector("[data-close]").onclick = () => dlg.close();
86
+ hydrateIcons(dlg);
87
+ };
88
+ if (!dlg) {
89
+ dlg = document.createElement("dialog");
90
+ dlg.id = "review-dialog"; dlg.className = "review-dialog";
91
+ document.body.append(dlg);
92
+ dlg.addEventListener("refresh", () => fill());
93
+ }
94
+ fill();
95
+ if (!dlg.open) dlg.showModal();
96
+ }
97
+ return { reviewKind, renderReviewBar, openReviewList, syncReviewBadge };
98
+ }
99
+ window.SagaReview = { create };
100
+ })();
@@ -0,0 +1,140 @@
1
+ // Edição visual sobre a imagem original. Só aplica alterações ao confirmar.
2
+ (function () {
3
+ "use strict";
4
+ window.ScreenshotEditor = { open };
5
+ function open(slide = {}, file) {
6
+ return new Promise(resolve => {
7
+ const draft = JSON.parse(JSON.stringify(slide));
8
+ let spots = draft.hotspots || [], selected = spots.length ? 0 : -1, mode = "point", image = draft.image || "", drag, loading = false;
9
+ const previous = document.activeElement;
10
+ const dlg = document.createElement("dialog");
11
+ dlg.className = "shot-dialog";
12
+ dlg.setAttribute("aria-labelledby", "shot-title");
13
+ dlg.innerHTML = `<header><div><small>SCREENSHOT INTERATIVO</small><h2 id="shot-title">Mostre exatamente onde olhar.</h2></div><button data-close aria-label="Fechar"><i class="ic" data-ic="x"></i></button></header>
14
+ <div class="shot-tools"><button data-file>Escolher imagem</button><span class="shot-divider"></span><button data-mode="point" aria-pressed="true"><i class="ic" data-ic="crosshair"></i> Ponto</button><button data-mode="area" aria-pressed="false"><i class="ic" data-ic="square-dashed"></i> Área</button><span class="shot-hint">Clique para marcar. Arraste um ponto para reposicionar.</span></div>
15
+ <div class="shot-work"><div class="shot-stage"><div class="shot-empty"><b>Cole seu screenshot aqui</b><span>Ctrl+V, arraste ou escolha uma imagem</span><small>PNG, JPEG ou WebP · até 8 MB</small></div><div class="shot-image-wrap" hidden><img alt="Screenshot para marcar"><div class="shot-marks"></div></div></div>
16
+ <aside><label>Título da cena<input data-title placeholder="Ex.: Configure seu workflow" maxlength="160"></label><label>Legenda da imagem<input data-caption placeholder="Opcional" maxlength="300"></label><div class="shot-steps-head"><b>Passos da explicação</b><span data-count>0</span></div><div class="shot-list"></div><div class="shot-fields" hidden><label>Destaque<input data-label maxlength="120" placeholder="Ex.: Clique em Executar"></label><label>Explicação<textarea data-text rows="3" placeholder="O que a pessoa precisa saber?"></textarea></label><div class="shot-order"><button data-up aria-label="Mover passo para cima"><i class="ic" data-ic="arrow-up"></i></button><button data-down aria-label="Mover passo para baixo"><i class="ic" data-ic="arrow-down"></i></button><button data-delete>Remover passo</button></div></div><p class="shot-tip">Na aula, um destaque por vez. No PDF, todos numerados com suas explicações.</p></aside></div>
17
+ <footer><span class="shot-status" role="status"></span><button data-cancel>Cancelar</button><button data-apply class="shot-primary">Usar screenshot</button></footer><input data-input type="file" accept="image/png,image/jpeg,image/webp" hidden>`;
18
+ document.body.appendChild(dlg);
19
+ // ícones desenhados (pacote do Studio), nunca caractere unicode
20
+ dlg.querySelectorAll(".ic[data-ic]").forEach((el) => { const svg = (window.UI_ICONS || {})[el.dataset.ic]; if (svg) el.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${svg}</svg>`; });
21
+ const $ = s => dlg.querySelector(s);
22
+ const img = $("img"), wrap = $(".shot-image-wrap"), stage = $(".shot-stage"), marks = $(".shot-marks");
23
+ $("[data-title]").value = draft.title || "";
24
+ $("[data-caption]").value = draft.caption || "";
25
+ const status = text => { $(".shot-status").textContent = text; };
26
+ function finish(value) { observer.disconnect(); dlg.close(); dlg.remove(); previous?.focus(); resolve(value); }
27
+ $("[data-close]").onclick = $("[data-cancel]").onclick = () => finish(null);
28
+ dlg.addEventListener("cancel", e => { e.preventDefault(); finish(null); });
29
+ dlg.addEventListener("keydown", e => {
30
+ e.stopPropagation();
31
+ if (!["Delete", "Backspace"].includes(e.key) || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;
32
+ if (e.target.closest("input, textarea, select, [contenteditable]")) return;
33
+ e.preventDefault();
34
+ removeSelected();
35
+ });
36
+ function fit() {
37
+ if (!img.naturalWidth) return;
38
+ const factor = Math.min((stage.clientWidth - 32) / img.naturalWidth, (stage.clientHeight - 32) / img.naturalHeight);
39
+ wrap.style.width = Math.max(1, img.naturalWidth * factor) + "px";
40
+ wrap.style.height = Math.max(1, img.naturalHeight * factor) + "px";
41
+ }
42
+ const observer = new ResizeObserver(fit); observer.observe(stage);
43
+ img.onload = () => { loading = false; wrap.hidden = false; $(".shot-empty").hidden = true; fit(); paint(); status("Imagem pronta. Marque os pontos importantes."); };
44
+ img.onerror = () => { loading = false; wrap.hidden = true; $(".shot-empty").hidden = false; status("Não foi possível abrir a imagem. Escolha o arquivo do screenshot."); };
45
+ async function read(file) {
46
+ if (!file) return;
47
+ if (!/^image\/(png|jpeg|webp)$/.test(file.type)) return status("Escolha uma imagem PNG, JPEG ou WebP.");
48
+ if (file.size > 8 * 1024 * 1024) return status("Essa imagem passa de 8 MB. Salve uma versão menor.");
49
+ if (image && spots.length && !window.confirm("Trocar a imagem remove os destaques atuais. Continuar?")) return;
50
+ loading = true;
51
+ status("Abrindo imagem…");
52
+ const reader = new FileReader();
53
+ reader.onerror = () => { loading = false; status("Não foi possível ler esse arquivo."); };
54
+ reader.onload = () => { image = reader.result; spots = []; selected = -1; img.src = image; };
55
+ reader.readAsDataURL(file);
56
+ }
57
+ $("[data-file]").onclick = () => $("[data-input]").click();
58
+ $("[data-input]").onchange = e => { read(e.target.files[0]); e.target.value = ""; };
59
+ dlg.addEventListener("paste", e => {
60
+ const item = [...(e.clipboardData?.items || [])].find(x => x.type.startsWith("image/"));
61
+ if (item) { e.preventDefault(); e.stopPropagation(); read(item.getAsFile()); }
62
+ });
63
+ dlg.addEventListener("dragover", e => { e.preventDefault(); });
64
+ dlg.addEventListener("drop", e => { e.preventDefault(); e.stopPropagation(); read(e.dataTransfer.files[0]); });
65
+ dlg.querySelectorAll("[data-mode]").forEach(b => b.onclick = () => {
66
+ mode = b.dataset.mode;
67
+ dlg.querySelectorAll("[data-mode]").forEach(x => x.setAttribute("aria-pressed", String(x === b)));
68
+ $(".shot-hint").textContent = mode === "point" ? "Clique para marcar. Arraste um ponto para reposicionar." : "Arraste para contornar a área importante.";
69
+ });
70
+ function paint() {
71
+ // redesenhar troca os botões dos destaques: quem estava com o foco continua com ele (senão, a imagem terminando
72
+ // de carregar no meio da edição "roubava" o foco e as setas do teclado paravam de mover o destaque)
73
+ const hadFocus = marks.contains(document.activeElement) ? +document.activeElement.dataset.index : -1;
74
+ marks.replaceChildren(); $(".shot-list").replaceChildren();
75
+ spots.forEach((p, i) => {
76
+ const mark = document.createElement("button"); mark.type = "button";
77
+ mark.className = `shot-mark ${p.kind === "point" ? "point" : "area"} ${selected === i ? "active" : ""}`;
78
+ mark.dataset.index = i; mark.textContent = i + 1; mark.setAttribute("aria-label", `Destaque ${i + 1}`);
79
+ Object.assign(mark.style, { left: `${p.x}%`, top: `${p.y}%`, width: p.kind === "point" ? "32px" : `${p.width}%`, height: p.kind === "point" ? "32px" : `${p.height}%` });
80
+ mark.onclick = e => { e.stopPropagation(); selected = i; paint(); marks.children[i]?.focus(); };
81
+ mark.onkeydown = e => {
82
+ const delta = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
83
+ if (!delta) return;
84
+ e.preventDefault(); p.x = Math.max(0, Math.min(100 - (p.kind === "point" ? 0 : p.width), p.x + delta[0])); p.y = Math.max(0, Math.min(100 - (p.kind === "point" ? 0 : p.height), p.y + delta[1])); paint(); marks.children[i].focus();
85
+ };
86
+ marks.appendChild(mark);
87
+ const row = document.createElement("button"); row.type = "button"; row.className = "shot-step" + (selected === i ? " active" : ""); row.textContent = `${i + 1}. ${p.title || "Sem legenda"}`; row.onclick = () => { selected = i; paint(); }; $(".shot-list").appendChild(row);
88
+ });
89
+ $("[data-count]").textContent = spots.length;
90
+ const p = spots[selected]; $(".shot-fields").hidden = !p;
91
+ if (p) { $("[data-label]").value = p.title || ""; $("[data-text]").value = p.text || ""; }
92
+ $("[data-up]").disabled = selected <= 0; $("[data-down]").disabled = selected < 0 || selected >= spots.length - 1;
93
+ if (hadFocus >= 0) marks.children[hadFocus]?.focus();
94
+ }
95
+ function coords(e) { const r = wrap.getBoundingClientRect(); return { x: Math.max(0, Math.min(100, (e.clientX - r.left) / r.width * 100)), y: Math.max(0, Math.min(100, (e.clientY - r.top) / r.height * 100)) }; }
96
+ wrap.onpointerdown = e => {
97
+ if (e.button !== 0) return;
98
+ e.preventDefault(); const pos = coords(e), target = e.target.closest("[data-index]");
99
+ if (target) { selected = +target.dataset.index; drag = { start: pos, index: selected, original: { ...spots[selected] } }; }
100
+ else if (spots.length >= 8) return status("Até 8 destaques por cena. Para mais passos, use outra cena.");
101
+ else { drag = { start: pos, index: spots.length, fresh: true }; spots.push({ kind: mode, x: pos.x, y: pos.y, width: 4, height: 4, title: `Passo ${spots.length + 1}`, text: "" }); selected = spots.length - 1; }
102
+ wrap.setPointerCapture(e.pointerId); paint();
103
+ };
104
+ wrap.onpointermove = e => {
105
+ if (!drag) return;
106
+ const pos = coords(e), p = spots[drag.index];
107
+ if (drag.fresh && mode === "area") { p.x = Math.min(pos.x, drag.start.x); p.y = Math.min(pos.y, drag.start.y); p.width = Math.max(2, Math.abs(pos.x - drag.start.x)); p.height = Math.max(2, Math.abs(pos.y - drag.start.y)); }
108
+ else if (!drag.fresh) { p.x = Math.max(0, Math.min(100 - (p.kind === "point" ? 0 : p.width), drag.original.x + pos.x - drag.start.x)); p.y = Math.max(0, Math.min(100 - (p.kind === "point" ? 0 : p.height), drag.original.y + pos.y - drag.start.y)); }
109
+ paint();
110
+ };
111
+ wrap.onpointerup = e => { if (drag) { const p = spots[drag.index]; if (p.kind !== "point") { p.x = Math.min(p.x, 96); p.y = Math.min(p.y, 96); p.width = Math.max(4, Math.min(p.width, 100 - p.x)); p.height = Math.max(4, Math.min(p.height, 100 - p.y)); } drag = null; wrap.releasePointerCapture(e.pointerId); paint(); marks.children[selected]?.focus(); } };
112
+ wrap.onpointercancel = () => { drag = null; };
113
+ $("[data-label]").oninput = e => { if (spots[selected]) { spots[selected].title = e.target.value; $(".shot-list").children[selected].textContent = `${selected + 1}. ${e.target.value || "Sem legenda"}`; } };
114
+ $("[data-text]").oninput = e => { if (spots[selected]) spots[selected].text = e.target.value; };
115
+ function removeSelected() {
116
+ if (selected < 0 || selected >= spots.length || drag) return;
117
+ spots.splice(selected, 1); selected = Math.min(selected, spots.length - 1); paint();
118
+ (marks.children[selected] || $("[data-mode='point']")).focus();
119
+ status("Destaque removido. Clique em Usar screenshot para confirmar ou Cancelar para descartar.");
120
+ }
121
+ $("[data-delete]").onclick = removeSelected;
122
+ $("[data-delete]").title = "Excluir destaque selecionado (Delete ou Backspace)";
123
+ for (const [sel, delta] of [["[data-up]", -1], ["[data-down]", 1]]) $(sel).onclick = () => { const to = selected + delta; if (to < 0 || to >= spots.length) return; [spots[selected], spots[to]] = [spots[to], spots[selected]]; selected = to; paint(); };
124
+ // Reabrir para editar recarrega a imagem: clicar em "Usar screenshot" nesse meio-tempo espera ela terminar (antes
125
+ // recusava com "Escolha uma imagem válida" e a edição se perdia sem aviso claro)
126
+ const loaded = () => new Promise((done) => { const t = setInterval(() => { if (!loading) { clearInterval(t); done(); } }, 40); });
127
+ $("[data-apply]").onclick = async () => {
128
+ if (loading) {
129
+ const b = $("[data-apply]"); b.disabled = true; status("Terminando de carregar a imagem…");
130
+ await loaded(); b.disabled = false;
131
+ }
132
+ if (!img.naturalWidth || !image) return status("Escolha uma imagem válida primeiro.");
133
+ const result = { ...draft, layout: spots.length ? "spotlight" : "image", fit: "contain", title: $("[data-title]").value.trim(), caption: $("[data-caption]").value.trim(), image, hotspots: spots };
134
+ delete result.figure; finish(result);
135
+ };
136
+ dlg.showModal(); paint();
137
+ if (file) read(file); else if (image) { loading = true; img.src = image; }
138
+ });
139
+ }
140
+ })();
@@ -0,0 +1,72 @@
1
+ // Arquivo › Compartilhar link (só leitura): cria, copia e revoga links para ver a apresentação aberta
2
+ // (src/studio/share-routes.js). Independente do app.js: só usa a API e a janela #modal-share do index.html.
3
+ (function () {
4
+ const $ = (id) => document.getElementById(id);
5
+ const icon = (name) => `<i class="ic" data-ic="${name}"><svg viewBox="0 0 24 24" aria-hidden="true">${(window.UI_ICONS || {})[name] || ""}</svg></i>`;
6
+ const absolute = (rel) => new URL(rel, location.href.replace(/[?#].*$/, "").replace(/\/(editor|biblioteca)\/?$/, "/")).href;
7
+ let multiuser = false;
8
+
9
+ async function copy(text, btn) {
10
+ try { await navigator.clipboard.writeText(text); }
11
+ catch { // http na rede (sem área de transferência do navegador): seleciona para o Ctrl+C
12
+ const input = btn.closest("li")?.querySelector("input");
13
+ input?.select(); document.execCommand?.("copy");
14
+ }
15
+ btn.classList.add("done"); btn.title = "Copiado";
16
+ setTimeout(() => { btn.classList.remove("done"); btn.title = "Copiar o link"; }, 1600);
17
+ }
18
+
19
+ function render(links) {
20
+ const list = $("share-list");
21
+ list.innerHTML = "";
22
+ for (const l of links) {
23
+ const url = absolute(l.path);
24
+ const li = document.createElement("li");
25
+ li.className = "share-item";
26
+ li.dataset.token = l.token;
27
+ const who = !multiuser ? "Quem alcança este Studio" : l.publico ? "Qualquer pessoa com o link" : "Só quem entra no portal";
28
+ li.innerHTML = `<span class="share-kind" title="${who}${l.notas ? " · com as notas" : ""}">${icon(!multiuser || !l.publico ? "lock" : "globe")}</span>
29
+ <input class="form-control share-url" readonly value="${url.replace(/"/g, "&quot;")}" aria-label="Link">
30
+ <button class="icon-btn share-copy" title="Copiar o link" aria-label="Copiar o link">${icon("copy")}</button>
31
+ <a class="icon-btn share-open" href="${url.replace(/"/g, "&quot;")}" target="_blank" rel="noopener noreferrer" title="Abrir como a pessoa vai ver" aria-label="Abrir">${icon("eye")}</a>
32
+ <button class="icon-btn share-revoke" title="Revogar: o link para de funcionar na hora" aria-label="Revogar">${icon("trash-2")}</button>`;
33
+ li.querySelector(".share-copy").onclick = (e) => copy(url, e.currentTarget);
34
+ li.querySelector(".share-url").onfocus = (e) => e.target.select();
35
+ li.querySelector(".share-revoke").onclick = async () => {
36
+ const r = await (await fetch("api/share/revoke", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: l.token }) })).json();
37
+ render(r.links || []);
38
+ };
39
+ list.appendChild(li);
40
+ }
41
+ }
42
+
43
+ async function open() {
44
+ const r = await (await fetch("api/share")).json(); // antes de mostrar: sem piscar a escolha de quem vê
45
+ multiuser = !!r.multiuser;
46
+ $("share-who").hidden = !multiuser; // no Studio local não há portal: o link abre para quem alcança o Studio
47
+ $("share-create").disabled = !r.saved;
48
+ $("share-create").title = r.saved ? $("share-create").dataset.hint || "" : "Salve a apresentação na biblioteca antes de compartilhar";
49
+ render(r.links || []);
50
+ $("modal-share").classList.remove("hidden");
51
+ }
52
+
53
+ async function create() {
54
+ const publico = multiuser && document.querySelector('input[name="share-who"]:checked')?.value === "publico";
55
+ const res = await fetch("api/share", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ publico, notas: $("share-notas").checked }) });
56
+ const r = await res.json();
57
+ if (!res.ok) { $("share-create").title = r.error || ""; return; }
58
+ render(r.links || []);
59
+ const first = [...$("share-list").children].find((li) => li.dataset.token === r.link.token);
60
+ first?.querySelector(".share-url")?.select();
61
+ }
62
+
63
+ function init() {
64
+ if (!$("menu-share") || !$("modal-share")) return;
65
+ $("menu-share").addEventListener("click", (e) => { e.preventDefault(); open(); });
66
+ $("share-close").onclick = () => $("modal-share").classList.add("hidden");
67
+ $("modal-share").addEventListener("click", (e) => { if (e.target === e.currentTarget) e.currentTarget.classList.add("hidden"); });
68
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !$("modal-share").classList.contains("hidden")) $("modal-share").classList.add("hidden"); });
69
+ $("share-create").onclick = create;
70
+ }
71
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init();
72
+ })();
@@ -0,0 +1,23 @@
1
+ window.SagaSlideClipboard = function ({ state, showToast, trackDeck, syncDeckToServer, renderThumbnails, renderCurrentSlide }) {
2
+ async function copyCurrentSlide() {
3
+ try {
4
+ const r = await fetch('api/slides/copy', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ slide:state.deck.slides[state.currentSlideIndex], expectFile:state.file }) });
5
+ const data = await r.json(); if(!r.ok || data.error) throw Error(data.error || r.status);
6
+ localStorage.setItem('sagadeck-slide-clipboard', data.token);
7
+ await navigator.clipboard?.writeText('sagadeck-slide:'+data.token).catch(()=>{});
8
+ showToast('Slide copiado. Abra outra apresentação e clique em Colar.');
9
+ } catch(e) { showToast(e.message,6000); }
10
+ }
11
+ async function pasteCopiedSlide() {
12
+ try {
13
+ const { token } = await (await fetch('api/slides/clipboard')).json();
14
+ if(!token) throw Error('Copie um slide primeiro.');
15
+ const r = await fetch('api/slides/paste', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({token,expectFile:state.file}) });
16
+ const data = await r.json(); if(!r.ok || data.error) throw Error(data.error || r.status);
17
+ const at = state.currentSlideIndex+1; state.deck.slides.splice(at,0,data.slide); state.currentSlideIndex=at;
18
+ trackDeck('Slide colado'); await syncDeckToServer(); renderThumbnails(); await renderCurrentSlide();
19
+ showToast('Slide colado com seus arquivos.');
20
+ } catch(e) { showToast(e.message,6000); }
21
+ }
22
+ return { copyCurrentSlide, pasteCopiedSlide };
23
+ };