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,827 @@
1
+ /* sagadeck · runtime do navegador (navegação, cliques/builds, apresentador, widgets) */
2
+ (function () {
3
+ "use strict";
4
+ const DATA = JSON.parse(document.getElementById("sagadeck-data").textContent);
5
+ const SW = 1920, SH = (DATA.size && DATA.size.h) || 1080; // tamanho lógico do slide (deck.aspect)
6
+ const KEY = "sagadeck:" + DATA.id;
7
+ const EXPORT = /[?&]export/.test(location.search) || location.hash === "#export";
8
+ const PRESENTER = location.hash.startsWith("#presenter");
9
+ const MOTION = ["none", "subtle", "expressive"].includes(DATA.motion) ? DATA.motion : "subtle";
10
+ document.documentElement.dataset.motion = MOTION;
11
+ const reducedMotion = () => MOTION === "none" || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
12
+ const $ = (s, r = document) => r.querySelector(s);
13
+ const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
14
+ const stage = $("#stage");
15
+ const slides = $$("#stage > .slide");
16
+ const N = slides.length;
17
+ let cur = 0, step = 0;
18
+ const hooks = slides.map(() => ({ enter: [], leave: [], step: [] }));
19
+
20
+ // ---------- armazenamento (localStorage pode falhar em file:// restrito) ----------
21
+ const store = {
22
+ get(k, d) { try { const v = localStorage.getItem(KEY + ":" + k); return v == null ? d : JSON.parse(v); } catch (e) { return d; } },
23
+ set(k, v) { try { localStorage.setItem(KEY + ":" + k, JSON.stringify(v)); } catch (e) {} broadcast({ type: "store", k }); },
24
+ };
25
+
26
+ // ---------- passos ----------
27
+ function stepsOf(s) {
28
+ let m = +s.dataset.steps || 0;
29
+ $$("[data-step]", s).forEach((e) => { m = Math.max(m, +e.dataset.step); });
30
+ $$("[data-exit]", s).forEach((e) => { m = Math.max(m, +e.dataset.exit); });
31
+ $$("[data-lesson]", s).forEach((e) => { m = Math.max(m, (+e.dataset.lessonCount || 1) - 1); });
32
+ return m;
33
+ }
34
+ const STEPS = slides.map(stepsOf);
35
+
36
+ function hiddenByStep(el, root) {
37
+ for (let p = el; p && p !== root; p = p.parentElement) {
38
+ if (p.dataset && p.dataset.step && !p.classList.contains("in")) return true;
39
+ if (p.classList && p.classList.contains("out")) return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function applyStep(s, k, opts = {}) {
45
+ // !! obrigatório: toggle(nome, undefined) INVERTE a classe em vez de desligar — "false || undefined"
46
+ // fazia cada clique inverter os itens ainda escondidos (tudo, 1º, tudo, último…).
47
+ $$("[data-step]", s).forEach((e) => e.classList.toggle("in", !!(k >= +e.dataset.step || opts.all)));
48
+ $$("[data-exit]", s).forEach((e) => e.classList.toggle("out", !opts.all && k >= +e.dataset.exit));
49
+ $$(".pl, mark, .chart", s).forEach((e) => {
50
+ if (!hiddenByStep(e, s)) play(e, opts.instant);
51
+ });
52
+ }
53
+
54
+ function resetPlay(s) {
55
+ $$(".play", s).forEach((e) => { e.classList.remove("play"); e.dataset.played = ""; });
56
+ $$(".timer", s).forEach(stopTimer);
57
+ $$(".counter", s).forEach((c) => { c.dataset.played = ""; });
58
+ }
59
+
60
+ function play(e, instant) {
61
+ if (e.classList.contains("play")) return;
62
+ e.classList.add("play");
63
+ if (e.classList.contains("counter")) runCounter(e, instant);
64
+ if (e.classList.contains("timer") && e.dataset.auto === "1" && !EXPORT) startTimer(e);
65
+ }
66
+
67
+ // ---------- contador ----------
68
+ function fmtNum(v, dec) { return v.toLocaleString("pt-BR", { minimumFractionDigits: dec, maximumFractionDigits: dec }); }
69
+ function runCounter(c, instant) {
70
+ const to = +c.dataset.to, from = +c.dataset.from, dec = +c.dataset.dec, cv = $(".cv", c);
71
+ const pre = c.dataset.prefix, suf = c.dataset.suffix;
72
+ if (instant || EXPORT || reducedMotion()) { cv.textContent = pre + fmtNum(to, dec) + suf; return; }
73
+ const t0 = performance.now(), dur = 1700;
74
+ const tick = (t) => {
75
+ const p = Math.min(1, (t - t0) / dur), e = 1 - Math.pow(2, -10 * p);
76
+ cv.textContent = pre + fmtNum(from + (to - from) * (p >= 1 ? 1 : e), dec) + suf;
77
+ if (p < 1 && c.classList.contains("play")) requestAnimationFrame(tick);
78
+ };
79
+ requestAnimationFrame(tick);
80
+ }
81
+
82
+ // ---------- timer ----------
83
+ function drawTimer(t, left) {
84
+ const total = +t.dataset.seconds;
85
+ $(".tv", t).textContent = Math.floor(left / 60) + ":" + String(Math.max(0, left % 60)).padStart(2, "0");
86
+ $(".tr-fg", t).style.strokeDashoffset = String(-(1 - left / total));
87
+ }
88
+ function startTimer(t) {
89
+ if (t._iv) return;
90
+ let left = t._left ?? +t.dataset.seconds;
91
+ drawTimer(t, left);
92
+ t._iv = setInterval(() => {
93
+ left = Math.max(0, left - 1); t._left = left; drawTimer(t, left);
94
+ if (left === 0) { clearInterval(t._iv); t._iv = null; t.classList.add("done"); }
95
+ }, 1000);
96
+ }
97
+ function stopTimer(t) { clearInterval(t._iv); t._iv = null; t._left = null; t.classList.remove("done"); drawTimer(t, +t.dataset.seconds); }
98
+ $$(".timer").forEach((t) => t.addEventListener("click", () => { if (t._iv) { clearInterval(t._iv); t._iv = null; } else startTimer(t); }));
99
+
100
+ // ---------- enquete ----------
101
+ function renderPoll(p) {
102
+ const id = p.dataset.poll, vals = store.get("poll:" + id, []);
103
+ const cmp = p.dataset.compare ? store.get("poll:" + p.dataset.compare, []) : null;
104
+ const rows = $$(".po-row", p);
105
+ const sum = vals.reduce((a, b) => a + (+b || 0), 0);
106
+ const csum = cmp ? cmp.reduce((a, b) => a + (+b || 0), 0) : 0;
107
+ rows.forEach((r, i) => {
108
+ const v = +vals[i] || 0, pct = sum ? (100 * v) / sum : 0;
109
+ $(".po-fill", r).style.width = pct + "%";
110
+ const valEl = $(".po-val", r);
111
+ if (!valEl.querySelector("input")) {
112
+ valEl.innerHTML = sum ? Math.round(pct) + "%" : "—";
113
+ if (cmp && csum && sum) {
114
+ const cp = (100 * (+cmp[i] || 0)) / csum, d = Math.round(pct - cp);
115
+ valEl.innerHTML += `<span class="po-delta">${d > 0 ? "+" : ""}${d} pts</span>`;
116
+ }
117
+ }
118
+ const g = $(".po-ghost", r);
119
+ if (cmp && csum) { g.style.left = (100 * (+cmp[i] || 0)) / csum + "%"; g.style.opacity = 1; } else g.style.opacity = 0;
120
+ });
121
+ }
122
+ $$(".poll").forEach((p) => {
123
+ renderPoll(p);
124
+ $$(".po-val", p).forEach((v, i) => v.addEventListener("click", () => {
125
+ if (v.querySelector("input")) return;
126
+ const vals = store.get("poll:" + p.dataset.poll, []);
127
+ v.innerHTML = `<input type="number" min="0" value="${vals[i] ?? ""}">`;
128
+ const inp = v.querySelector("input"); inp.focus(); inp.select();
129
+ const done = () => { const vs = store.get("poll:" + p.dataset.poll, []); vs[i] = inp.value === "" ? 0 : +inp.value; store.set("poll:" + p.dataset.poll, vs); v.innerHTML = ""; $$(".poll").forEach(renderPoll); };
130
+ inp.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); done(); const nx = $$(".po-val", p)[i + 1]; if (nx && e.key === "Tab") nx.click(); } if (e.key === "Escape") { v.innerHTML = ""; renderPoll(p); } });
131
+ inp.addEventListener("blur", () => { if (v.querySelector("input")) done(); });
132
+ }));
133
+ });
134
+
135
+ // ---------- ajuste para caber (src/runtime/fit.js, o mesmo código do Studio) ----------
136
+ function fitAll(root = document) {
137
+ window.SagadeckFit.fitText(root);
138
+ $$(".slide", root).forEach((s) => window.SagadeckFit.shrink(s));
139
+ }
140
+
141
+ // ---------- escala ----------
142
+ function scale() {
143
+ const k = Math.min(innerWidth / SW, innerHeight / SH);
144
+ stage.style.transform = `scale(${k})`;
145
+ }
146
+
147
+ // ---------- navegação ----------
148
+ // fora do slide (tela de outra proporção): a cor do slide, que emenda sem borda; slide com moldura até a borda (o
149
+ // original importado, o estilo tirado do PowerPoint) fica com fundo escuro, como no PowerPoint (a faixa azul da capa
150
+ // acabava no meio de uma área clara)
151
+ function setBg() {
152
+ const s = slides[cur];
153
+ const bg = s.matches('[data-layout="canvas"], .has-master') ? "#0b0b0b" : getComputedStyle(s).backgroundColor;
154
+ $("#viewport").style.background = bg;
155
+ document.body.style.background = bg;
156
+ }
157
+ function goto(i, k = 0, opts = {}) {
158
+ i = Math.max(0, Math.min(N - 1, i));
159
+ k = Math.max(0, Math.min(STEPS[i], k));
160
+ const changed = i !== cur || opts.force;
161
+ const origins = new Map();
162
+ if (changed && !opts.instant && !EXPORT && !reducedMotion()) {
163
+ slides[cur].querySelectorAll('[data-continuity]').forEach(el => {
164
+ const r = el.getBoundingClientRect();
165
+ if (r.width && r.height && getComputedStyle(el).visibility !== 'hidden') origins.set(el.dataset.continuity, r);
166
+ });
167
+ }
168
+ if (changed) {
169
+ const prev = slides[cur];
170
+ const incoming = slides[i].dataset.tr === "saida" || slides[i].dataset.tr === "morph" ? slides[i].dataset.tr : "";
171
+ const animated = incoming && !opts.instant && !EXPORT && !reducedMotion();
172
+ prev.classList.remove("current");
173
+ if (animated) {
174
+ const leaving = "tr-leaving-" + incoming;
175
+ prev.classList.add(leaving);
176
+ setTimeout(() => prev.classList.remove(leaving), incoming === "morph" ? 750 : 500);
177
+ }
178
+ hooks[cur].leave.forEach((f) => f());
179
+ resetPlay(prev);
180
+ cur = i;
181
+ slides.forEach((s, j) => s.classList.toggle("current", j === i));
182
+ if (opts.instant) { slides[i].style.transition = "none"; slides[i].style.animation = "none"; }
183
+ redrawSlideDrawings(i);
184
+ }
185
+ step = k;
186
+ applyStep(slides[i], k, opts);
187
+ if (origins.size) requestAnimationFrame(() => {
188
+ if (cur !== i) return;
189
+ const scale = stage.getBoundingClientRect().width / SW || 1;
190
+ slides[i].querySelectorAll('[data-continuity]').forEach(el => {
191
+ const from = origins.get(el.dataset.continuity), to = el.getBoundingClientRect();
192
+ if (!from || !to.width || !to.height) return;
193
+ const transform = getComputedStyle(el).transform;
194
+ el.animate([
195
+ { transform: `translate(${(from.x-to.x)/scale}px,${(from.y-to.y)/scale}px) scale(${from.width/to.width},${from.height/to.height})`, transformOrigin: '0 0' },
196
+ { transform: transform === 'none' ? 'none' : transform, transformOrigin: '0 0' }
197
+ ], { duration: 550, easing: 'cubic-bezier(.2,.7,.2,1)' });
198
+ });
199
+ });
200
+ if (changed) hooks[i].enter.forEach((f) => f(k));
201
+ hooks[i].step.forEach((f) => f(k));
202
+ if (opts.instant) { void slides[i].offsetWidth; slides[i].style.transition = ""; slides[i].style.animation = ""; }
203
+ setBg();
204
+ const bar = $("#hud .bar"); if (bar) bar.style.width = ((i + (STEPS[i] ? k / (STEPS[i] + 1) : 0)) / Math.max(1, N - 1)) * 100 + "%";
205
+ if (!EXPORT && !PRESENTER && location.href!=='about:srcdoc') history.replaceState(null, "", "#" + (i + 1) + (k ? "." + k : ""));
206
+ broadcast({ type: "state", i, k });
207
+ if (PRESENTER && document.getElementById("pv")) presenterState(i, k);
208
+ }
209
+ // Navegação por caminhos: id do slide (data-id) ou número; next: no slide troca o "próximo" do fim dele
210
+ const slideOf = (target) => {
211
+ const t = String(target ?? "").replace(/^#?s-/, "");
212
+ const j = slides.findIndex((s) => s.dataset.id === t);
213
+ return j >= 0 ? j : /^\d+$/.test(t) ? +t - 1 : -1;
214
+ };
215
+ const next = () => {
216
+ if (step < STEPS[cur]) return goto(cur, step + 1);
217
+ const to = slides[cur].dataset.next ? slideOf(slides[cur].dataset.next) : -1;
218
+ if (to >= 0) return goto(to, 0);
219
+ return cur < N - 1 && goto(cur + 1, 0);
220
+ };
221
+ const prev = () => (step > 0 ? goto(cur, step - 1) : cur > 0 && goto(cur - 1, STEPS[cur - 1], { instant: true }));
222
+
223
+ // ---------- mensagens com a janela do apresentador ----------
224
+ let pwin = null;
225
+ function broadcast(msg) {
226
+ try { if (pwin && !pwin.closed) pwin.postMessage({ sagadeck: DATA.id, ...msg }, "*"); } catch (e) {}
227
+ try { if (PRESENTER && window.opener) window.opener.postMessage({ sagadeck: DATA.id, ...msg }, "*"); } catch (e) {}
228
+ }
229
+ function openPresenter() {
230
+ pwin = window.open(location.href.split("#")[0] + "#presenter", "sagadeck-apresentador", "width=1400,height=860");
231
+ toast(pwin ? "Janela do apresentador aberta" : "O navegador bloqueou a janela (libere pop-ups)");
232
+ }
233
+
234
+ // ---------- UI ----------
235
+ function toast(t) { const el = $("#toast"); el.textContent = t; el.classList.add("show"); clearTimeout(el._t); el._t = setTimeout(() => el.classList.remove("show"), 1800); }
236
+ function overview() {
237
+ const ov = $("#overview");
238
+ if (document.body.classList.toggle("ov")) {
239
+ ov.innerHTML = "";
240
+ slides.forEach((s, j) => {
241
+ const it = document.createElement("div"); it.className = "ov-item" + (j === cur ? " cur" : "");
242
+ const sc = document.createElement("div"); sc.className = "ov-scale";
243
+ const cl = s.cloneNode(true); cl.classList.add("current"); applyStatic(cl, STEPS[j]);
244
+ sc.appendChild(cl); it.appendChild(sc);
245
+ it.insertAdjacentHTML("beforeend", `<span class="ov-n">${j + 1}</span>`);
246
+ it.onclick = () => { document.body.classList.remove("ov"); goto(j, 0); };
247
+ ov.appendChild(it);
248
+ requestAnimationFrame(() => { sc.style.transform = `scale(${it.clientWidth / 1920})`; });
249
+ });
250
+ $(".ov-item.cur", ov)?.scrollIntoView({ block: "center" });
251
+ }
252
+ }
253
+ function applyStatic(node, k) {
254
+ $$("[data-step]", node).forEach((e) => e.classList.toggle("in", k >= +e.dataset.step));
255
+ $$("[data-exit]", node).forEach((e) => e.classList.toggle("out", k >= +e.dataset.exit));
256
+ $$(".pl, mark, .chart", node).forEach((e) => e.classList.add("play"));
257
+ $$(".e", node).forEach((e) => (e.style.animation = "none"));
258
+ $$(".counter", node).forEach((c) => { $(".cv", c).textContent = c.dataset.prefix + fmtNum(+c.dataset.to, +c.dataset.dec) + c.dataset.suffix; });
259
+ $$("[data-lesson]", node).forEach((root) => renderLesson(root, k));
260
+ }
261
+
262
+ // ---------- cenas de aula: o clique da apresentação também é a etapa da explicação ----------
263
+ function kineticMorph(root, source, target) {
264
+ if (reducedMotion() || !source) return;
265
+ const word = $(".kinetic-word", target);
266
+ const sequence = $(".kinetic-sequence", root);
267
+ if (!word || !sequence || typeof word.animate !== "function") return;
268
+ const rootRect = root.getBoundingClientRect(), fromRect = source.rect, toRect = word.getBoundingClientRect();
269
+ if (!rootRect.width || !rootRect.height || !fromRect.width || !toRect.width) return;
270
+ const scaleX = root.offsetWidth / rootRect.width, scaleY = root.offsetHeight / rootRect.height;
271
+ const ghost = source.element.cloneNode(true);
272
+ ghost.classList.add("kinetic-morph");
273
+ ghost.removeAttribute("data-fit");
274
+ ghost.setAttribute("aria-hidden", "true");
275
+ const style = source.style;
276
+ Object.assign(ghost.style, {
277
+ position: "absolute",
278
+ left: `${(fromRect.left - rootRect.left) * scaleX}px`,
279
+ top: `${(fromRect.top - rootRect.top) * scaleY}px`,
280
+ width: `${fromRect.width * scaleX}px`,
281
+ height: "auto",
282
+ maxHeight: "none",
283
+ margin: "0",
284
+ overflow: "visible",
285
+ transform: "none",
286
+ transformOrigin: "top left",
287
+ zIndex: "4",
288
+ pointerEvents: "none",
289
+ fontFamily: style.fontFamily,
290
+ fontSize: style.fontSize,
291
+ fontWeight: style.fontWeight,
292
+ fontStyle: style.fontStyle,
293
+ lineHeight: style.lineHeight,
294
+ letterSpacing: style.letterSpacing,
295
+ textTransform: style.textTransform,
296
+ textAlign: style.textAlign,
297
+ color: style.color,
298
+ textShadow: style.textShadow,
299
+ background: style.background,
300
+ padding: style.padding,
301
+ borderRadius: style.borderRadius,
302
+ boxSizing: "border-box",
303
+ });
304
+ sequence.append(ghost);
305
+ const dx = (toRect.left - fromRect.left) * scaleX, dy = (toRect.top - fromRect.top) * scaleY;
306
+ const morphScale = Math.max(.72, Math.min(1.38, toRect.width / fromRect.width));
307
+ const animation = ghost.animate([
308
+ { opacity: .92, transform: "translate(0, 0) scale(1)", filter: "blur(0)" },
309
+ { opacity: .82, offset: .62, transform: `translate(${dx * .82}px, ${dy * .82}px) scale(${1 + (morphScale - 1) * .82})`, filter: "blur(0)" },
310
+ { opacity: 0, transform: `translate(${dx}px, ${dy}px) scale(${morphScale})`, filter: "blur(4px)" },
311
+ ], { duration: 720, easing: "cubic-bezier(.22,.78,.22,1)", fill: "both" });
312
+ animation.finished.then(() => ghost.remove(), () => ghost.remove());
313
+ }
314
+
315
+ function renderLesson(root, requested) {
316
+ const n = +root.dataset.lessonCount || 1, index = Math.max(0, Math.min(n - 1, +requested || 0));
317
+ const previous = Number(root.dataset.lessonIndex);
318
+ const oldPanel = root.dataset.lesson === "kinetic" && Number.isInteger(previous)
319
+ ? $(`[data-lesson-panel="${previous}"].active`, root) : null;
320
+ const oldWord = oldPanel && $(".kinetic-word", oldPanel);
321
+ const source = oldWord ? { element: oldWord, rect: oldWord.getBoundingClientRect(), style: getComputedStyle(oldWord) } : null;
322
+ root.dataset.lessonIndex = String(index);
323
+ // peças que acompanham o quadro sem serem painéis (carrossel): o CSS gira/desliza com --lesson-i e active/past
324
+ root.style.setProperty("--lesson-i", String(index));
325
+ $$("[data-lesson-k]", root).forEach((el) => {
326
+ const k = +el.dataset.lessonK;
327
+ el.classList.toggle("active", k === index);
328
+ el.classList.toggle("past", k < index);
329
+ });
330
+ $$("[data-lesson-panel]", root).forEach((panel, i) => {
331
+ panel.classList.toggle("active", i === index);
332
+ panel.setAttribute("aria-hidden", String(i !== index));
333
+ });
334
+ $$("[data-lesson-go]", root).forEach((button) => {
335
+ const active = +button.dataset.lessonGo === index;
336
+ button.classList.toggle("active", active);
337
+ button.setAttribute("aria-current", active ? "step" : "false");
338
+ });
339
+ const prev = $("[data-lesson-prev]", root), next = $("[data-lesson-next]", root);
340
+ if (prev) prev.disabled = index === 0;
341
+ if (next) next.disabled = index === n - 1;
342
+ const panel = $(".lesson-panel.active", root);
343
+ const highlight = new Set(JSON.parse(panel?.dataset.highlight || "[]"));
344
+ $$(".code .cl", root).forEach((line, i) => {
345
+ line.classList.toggle("hl", highlight.has(i + 1));
346
+ line.classList.toggle("dim", highlight.size > 0 && !highlight.has(i + 1));
347
+ });
348
+ if (root.dataset.lesson === "codewalk") {
349
+ const code = $(".code", root), lines = $$(".code .cl.hl", root);
350
+ if (code && lines.length && code.getClientRects().length) {
351
+ const box = code.getBoundingClientRect(), first = lines[0].getBoundingClientRect(), last = lines.at(-1).getBoundingClientRect();
352
+ const scale = box.width / code.offsetWidth || 1;
353
+ if (first.top < box.top || last.bottom > box.bottom) {
354
+ const offset = last.bottom - first.top > box.height
355
+ ? first.top - box.top : (first.top + last.bottom) / 2 - box.top - box.height / 2;
356
+ code.scrollTop += offset / scale;
357
+ }
358
+ }
359
+ }
360
+ if (root.dataset.lesson === "solution") window.SagadeckFit.fitSteps(root); // o quadro novo só mede agora, à vista
361
+ if (root.dataset.lesson === "kinetic") {
362
+ window.SagadeckFit.fitText(root);
363
+ if (previous !== index) kineticMorph(root, source, $(`[data-lesson-panel="${index}"]`, root));
364
+ }
365
+ }
366
+ function fitSpotlight(root) {
367
+ const canvas = $(".spotlight-canvas", root), img = $(".spotlight-image img", root), regions = $(".spotlight-regions", root);
368
+ if (!canvas || !img?.naturalWidth || !regions) return;
369
+ // As regiões seguem a imagem real, inclusive com barras laterais em um screenshot vertical.
370
+ const scale = Math.min(canvas.clientWidth / img.naturalWidth, canvas.clientHeight / img.naturalHeight);
371
+ const width = img.naturalWidth * scale, height = img.naturalHeight * scale;
372
+ regions.style.cssText = `width:${width}px;height:${height}px;left:${(canvas.clientWidth - width) / 2}px;top:${(canvas.clientHeight - height) / 2}px;`;
373
+ zoomSpotlight(root);
374
+ }
375
+ function zoomSpotlight(root) {
376
+ const canvas = $(".spotlight-canvas", root), layer = $(".spotlight-zoom", root), regions = $(".spotlight-regions", root);
377
+ const region = $(".spotlight-region.active", root);
378
+ if (!canvas || !layer || !regions) return;
379
+ const s = EXPORT ? 1 : +(region?.dataset.zoom || 1);
380
+ if (!(s > 1)) { layer.style.transform = ""; layer.style.setProperty("--zs", "1"); return; }
381
+ const W = canvas.clientWidth, H = canvas.clientHeight;
382
+ // centro do foco em pixels do canvas (as regiões são % da área real da imagem)
383
+ const rx = regions.offsetLeft + (region.offsetLeft + region.offsetWidth / 2), ry = regions.offsetTop + (region.offsetTop + region.offsetHeight / 2);
384
+ const tx = Math.min(0, Math.max(W - W * s, W / 2 - rx * s)), ty = Math.min(0, Math.max(H - H * s, H / 2 - ry * s));
385
+ layer.style.setProperty("--zs", String(s));
386
+ layer.style.transform = `translate(${tx}px, ${ty}px) scale(${s})`;
387
+ }
388
+ function mountLessons() {
389
+ slides.forEach((slide, si) => $$("[data-lesson]", slide).forEach((root) => {
390
+ renderLesson(root, 0);
391
+ // botão Tocar (algoritmo animado e afins): avança os passos sozinho; clicar de novo, trocar de slide ou chegar ao fim para
392
+ $$("[data-autoplay]", root).forEach((btn) => {
393
+ let timer = null;
394
+ const ms = Math.max(250, +root.dataset.autoplayMs || 900);
395
+ const stop = () => { clearInterval(timer); timer = null; root.classList.remove("playing"); btn.setAttribute("aria-pressed", "false"); };
396
+ btn.addEventListener("click", (e) => {
397
+ e.stopPropagation();
398
+ if (timer) return stop();
399
+ if (+root.dataset.lessonIndex >= +root.dataset.lessonCount - 1) goto(si, 0);
400
+ root.classList.add("playing"); btn.setAttribute("aria-pressed", "true");
401
+ timer = setInterval(() => {
402
+ const i = +root.dataset.lessonIndex;
403
+ if (cur !== si || i >= +root.dataset.lessonCount - 1) return stop();
404
+ goto(si, i + 1);
405
+ }, ms);
406
+ });
407
+ hooks[si].leave.push(stop);
408
+ });
409
+ hooks[si].step.push((k) => { renderLesson(root, k); fitSpotlight(root); zoomSpotlight(root); });
410
+ if (root.dataset.lesson === "kinetic") {
411
+ let timer = null;
412
+ let enabled = root.dataset.kineticAutoplay === "true" && !reducedMotion() && +root.dataset.lessonCount > 1;
413
+ const toggle = $("[data-kinetic-toggle]", root);
414
+ const stop = () => { clearInterval(timer); timer = null; };
415
+ const updateToggle = () => {
416
+ if (!toggle) return;
417
+ toggle.textContent = enabled ? "Pausar" : "Reproduzir";
418
+ toggle.setAttribute("aria-label", enabled ? "Pausar sequência" : "Reproduzir sequência");
419
+ toggle.setAttribute("aria-pressed", String(enabled));
420
+ };
421
+ const start = () => {
422
+ if (!enabled || EXPORT || timer || +root.dataset.lessonIndex >= +root.dataset.lessonCount - 1) return;
423
+ const interval = Math.max(450, Math.min(5000, +root.dataset.kineticInterval || 1000));
424
+ timer = setInterval(() => {
425
+ const index = +root.dataset.lessonIndex || 0;
426
+ if (index >= +root.dataset.lessonCount - 1) { stop(); return; }
427
+ goto(si, index + 1);
428
+ }, interval);
429
+ };
430
+ toggle?.addEventListener("click", (event) => {
431
+ event.stopPropagation();
432
+ enabled = !enabled;
433
+ if (enabled && +root.dataset.lessonIndex >= +root.dataset.lessonCount - 1) goto(si, 0);
434
+ updateToggle();
435
+ if (enabled) start(); else stop();
436
+ });
437
+ hooks[si].enter.push(start);
438
+ hooks[si].leave.push(stop);
439
+ hooks[si].step.push((index) => {
440
+ if (index >= +root.dataset.lessonCount - 1 && enabled) {
441
+ stop();
442
+ enabled = false;
443
+ updateToggle();
444
+ }
445
+ });
446
+ updateToggle();
447
+ }
448
+ root.addEventListener("click", (event) => {
449
+ const button = event.target.closest("button");
450
+ if (!button || !root.contains(button) || button.disabled) return;
451
+ const index = +root.dataset.lessonIndex || 0;
452
+ let target;
453
+ if (button.hasAttribute("data-lesson-go")) target = +button.dataset.lessonGo;
454
+ else if (button.hasAttribute("data-lesson-prev")) target = index - 1;
455
+ else if (button.hasAttribute("data-lesson-next")) target = index + 1;
456
+ if (target == null) return;
457
+ event.stopPropagation();
458
+ goto(si, target);
459
+ });
460
+ // Enter/Espaço em um botão deve acioná-lo; setas continuam a navegação da apresentação.
461
+ root.addEventListener("keydown", (event) => {
462
+ if (event.target.closest("button") && ["Enter", " "].includes(event.key)) event.stopPropagation();
463
+ });
464
+ root.addEventListener("touchstart", (event) => event.stopPropagation(), { passive: true });
465
+ root.addEventListener("touchend", (event) => event.stopPropagation(), { passive: true });
466
+ const img = $(".spotlight-image img", root);
467
+ if (img) img.addEventListener("load", () => fitSpotlight(root));
468
+ fitSpotlight(root);
469
+ }));
470
+ window.addEventListener("resize", () => $$("[data-lesson='spotlight']").forEach(fitSpotlight));
471
+ }
472
+
473
+ // ---------- caneta e anotações ao vivo (live drawing) ----------
474
+ const drawCanvas = $("#draw-canvas");
475
+ const drawCtx = drawCanvas ? drawCanvas.getContext("2d") : null;
476
+ let isDrawingMode = false;
477
+ let drawTool = "pen"; // "pen" | "highlighter"
478
+ let drawColor = "#ef4444";
479
+ let isDrawingDown = false;
480
+ let activeStroke = null;
481
+ const slideDrawings = {}; // { [slideIndex]: [strokes] }
482
+
483
+ function toggleDrawing(forceState, tool = "pen") {
484
+ if (!drawCanvas) return;
485
+ isDrawingMode = typeof forceState === "boolean" ? forceState : !isDrawingMode;
486
+ if (tool) drawTool = tool;
487
+ document.body.classList.toggle("drawing", isDrawingMode);
488
+ const tb = $("#draw-toolbar");
489
+ const fab = $("#draw-fab");
490
+ if (tb) tb.style.display = isDrawingMode ? "flex" : "none";
491
+ if (fab) fab.style.display = isDrawingMode ? "none" : "";
492
+ if (isDrawingMode) {
493
+ updateDrawUI();
494
+ toast(drawTool === "highlighter" ? "Marca-texto ativo (D: caneta, C: limpar)" : "Caneta ativa (M: marca-texto, C: limpar)");
495
+ }
496
+ }
497
+
498
+ function updateDrawUI() {
499
+ $("#draw-btn-pen")?.classList.toggle("active", drawTool === "pen");
500
+ $("#draw-btn-highlighter")?.classList.toggle("active", drawTool === "highlighter");
501
+ $$(".draw-color").forEach((b) => b.classList.toggle("active", b.dataset.color === drawColor));
502
+ }
503
+
504
+ function getDrawCoords(e) {
505
+ const rect = drawCanvas.getBoundingClientRect();
506
+ const sx = 1920 / rect.width;
507
+ const sy = SH / rect.height;
508
+ return {
509
+ x: (e.clientX - rect.left) * sx,
510
+ y: (e.clientY - rect.top) * sy,
511
+ };
512
+ }
513
+
514
+ function renderStroke(s) {
515
+ if (!drawCtx || !s.points || s.points.length < 2) return;
516
+ drawCtx.save();
517
+ drawCtx.lineCap = "round";
518
+ drawCtx.lineJoin = "round";
519
+ if (s.tool === "highlighter") {
520
+ drawCtx.globalAlpha = 0.38;
521
+ drawCtx.strokeStyle = s.color;
522
+ drawCtx.lineWidth = s.width || 28;
523
+ } else {
524
+ drawCtx.globalAlpha = 1.0;
525
+ drawCtx.strokeStyle = s.color;
526
+ drawCtx.lineWidth = s.width || 5;
527
+ }
528
+
529
+ drawCtx.beginPath();
530
+ drawCtx.moveTo(s.points[0].x, s.points[0].y);
531
+ for (let i = 1; i < s.points.length; i++) {
532
+ const p1 = s.points[i - 1];
533
+ const p2 = s.points[i];
534
+ const mx = (p1.x + p2.x) / 2;
535
+ const my = (p1.y + p2.y) / 2;
536
+ drawCtx.quadraticCurveTo(p1.x, p1.y, mx, my);
537
+ }
538
+ const last = s.points[s.points.length - 1];
539
+ drawCtx.lineTo(last.x, last.y);
540
+ drawCtx.stroke();
541
+ drawCtx.restore();
542
+ }
543
+
544
+ function redrawSlideDrawings(slideIdx) {
545
+ if (!drawCtx) return;
546
+ drawCtx.clearRect(0, 0, SW, SH);
547
+ const strokes = slideDrawings[slideIdx] || [];
548
+ for (const s of strokes) {
549
+ renderStroke(s);
550
+ }
551
+ }
552
+
553
+ function undoDrawing() {
554
+ const strokes = slideDrawings[cur] || [];
555
+ if (strokes.length > 0) {
556
+ strokes.pop();
557
+ redrawSlideDrawings(cur);
558
+ toast("Último traço desfeito");
559
+ }
560
+ }
561
+
562
+ function clearDrawing() {
563
+ slideDrawings[cur] = [];
564
+ if (drawCtx) drawCtx.clearRect(0, 0, SW, SH);
565
+ toast("Anotações do slide limpas");
566
+ }
567
+
568
+ function initDrawing() {
569
+ if (!drawCanvas) return;
570
+ drawCanvas.addEventListener("pointerdown", (e) => {
571
+ if (!isDrawingMode) return;
572
+ e.preventDefault();
573
+ isDrawingDown = true;
574
+ const pt = getDrawCoords(e);
575
+ activeStroke = {
576
+ tool: drawTool,
577
+ color: drawColor,
578
+ width: drawTool === "highlighter" ? 28 : 5,
579
+ points: [pt, pt],
580
+ };
581
+ if (!slideDrawings[cur]) slideDrawings[cur] = [];
582
+ slideDrawings[cur].push(activeStroke);
583
+ renderStroke(activeStroke);
584
+ });
585
+
586
+ drawCanvas.addEventListener("pointermove", (e) => {
587
+ if (!isDrawingDown || !activeStroke) return;
588
+ e.preventDefault();
589
+ const pt = getDrawCoords(e);
590
+ activeStroke.points.push(pt);
591
+ redrawSlideDrawings(cur);
592
+ });
593
+
594
+ const endDraw = () => {
595
+ isDrawingDown = false;
596
+ activeStroke = null;
597
+ };
598
+ drawCanvas.addEventListener("pointerup", endDraw);
599
+ drawCanvas.addEventListener("pointercancel", endDraw);
600
+
601
+ $("#draw-fab")?.addEventListener("click", () => toggleDrawing(true, "pen"));
602
+ $("#draw-btn-pen")?.addEventListener("click", () => { drawTool = "pen"; updateDrawUI(); });
603
+ $("#draw-btn-highlighter")?.addEventListener("click", () => { drawTool = "highlighter"; updateDrawUI(); });
604
+ $("#draw-btn-undo")?.addEventListener("click", undoDrawing);
605
+ $("#draw-btn-clear")?.addEventListener("click", clearDrawing);
606
+ $("#draw-btn-close")?.addEventListener("click", () => toggleDrawing(false));
607
+ $$(".draw-color").forEach((btn) => {
608
+ btn.addEventListener("click", () => {
609
+ drawColor = btn.dataset.color || "#ef4444";
610
+ updateDrawUI();
611
+ });
612
+ });
613
+ }
614
+
615
+ let jump = "";
616
+ function onKey(e) {
617
+ if (e.target.closest && e.target.closest("input,textarea,[contenteditable]")) return;
618
+ if ((e.ctrlKey || e.metaKey) && (e.key === "z" || e.key === "Z") && isDrawingMode) {
619
+ e.preventDefault();
620
+ undoDrawing();
621
+ return;
622
+ }
623
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
624
+ const k = e.key;
625
+ if (/^[0-9]$/.test(k)) { jump += k; return; }
626
+ if (k === "Enter" && jump) { goto(+jump - 1, 0); jump = ""; return; }
627
+ jump = "";
628
+ if (["ArrowRight", "ArrowDown", "PageDown", " ", "Enter"].includes(k)) { e.preventDefault(); cmd("next"); }
629
+ else if (["ArrowLeft", "ArrowUp", "PageUp", "Backspace"].includes(k)) { e.preventDefault(); cmd("prev"); }
630
+ else if (k === "Home") cmd("first");
631
+ else if (k === "End") cmd("last");
632
+ else if (k === "f" || k === "F") { if (document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen?.(); }
633
+ else if (k === "p" || k === "P") { if (!PRESENTER) openPresenter(); }
634
+ else if (k === "g" || k === "G") { if (!PRESENTER) overview(); }
635
+ else if (k === "Escape") {
636
+ if (isDrawingMode) toggleDrawing(false);
637
+ document.body.classList.remove("ov", "help", "blank", "white", "laser");
638
+ }
639
+ else if (k === "b" || k === "B" || k === ".") cmd("blank");
640
+ else if (k === "w" || k === "W") cmd("white");
641
+ else if (k === "l" || k === "L") { document.body.classList.toggle("laser"); }
642
+ else if (k === "d" || k === "D") { toggleDrawing(isDrawingMode && drawTool === "pen" ? false : true, "pen"); }
643
+ else if (k === "m" || k === "M") { toggleDrawing(isDrawingMode && drawTool === "highlighter" ? false : true, "highlighter"); }
644
+ else if (isDrawingMode && (k === "c" || k === "C" || k === "e" || k === "E")) { clearDrawing(); }
645
+ else if (isDrawingMode && (k === "z" || k === "Z")) { undoDrawing(); }
646
+ else if (k === "h" || k === "H" || k === "?") document.body.classList.toggle("help");
647
+ else if (k === "r" || k === "R") { $$(".timer", slides[cur]).forEach((t) => { stopTimer(t); }); toast("Timer zerado"); }
648
+ else if (k.length === 1) cmd("key", k);
649
+ }
650
+ // clicar num item com goto (cartão, opção do mapa, item de lista, [texto](#id), Voltar) leva ao slide dele
651
+ document.addEventListener("click", (e) => {
652
+ if (EXPORT) return;
653
+ const a = e.target.closest && e.target.closest("[data-goto]");
654
+ if (!a || !a.closest(".slide")) return;
655
+ const to = slideOf(a.dataset.goto);
656
+ if (to < 0) return;
657
+ e.preventDefault();
658
+ e.stopPropagation();
659
+ cmd("goto", [to, 0]);
660
+ }, true);
661
+
662
+ function cmd(c, arg) {
663
+ if (PRESENTER && window.opener && !window.opener.closed) { window.opener.postMessage({ sagadeck: DATA.id, type: "cmd", c, arg }, "*"); return; }
664
+ local(c, arg);
665
+ }
666
+ function local(c, arg) {
667
+ if (c === "next") next(); else if (c === "prev") prev();
668
+ else if (c === "first") goto(0, 0); else if (c === "last") goto(N - 1, 0);
669
+ else if (c === "goto") goto(arg[0], arg[1]);
670
+ else if (c === "blank") { document.body.classList.remove("white"); document.body.classList.toggle("blank"); }
671
+ else if (c === "white") { document.body.classList.remove("blank"); document.body.classList.toggle("white"); }
672
+ else if (c === "key") { (hooks[cur].keys || []).forEach((f) => f(arg)); }
673
+ }
674
+ window.addEventListener("message", (ev) => {
675
+ const m = ev.data; if (!m || m.sagadeck !== DATA.id) return;
676
+ if (m.type === "cmd" && !PRESENTER) local(m.c, m.arg);
677
+ if (m.type === "hello" && !PRESENTER) { pwin = ev.source; broadcast({ type: "state", i: cur, k: step }); }
678
+ if (m.type === "state" && PRESENTER) presenterState(m.i, m.k);
679
+ if (m.type === "store") $$(".poll").forEach(renderPoll);
680
+ });
681
+
682
+ // ---------- widgets ----------
683
+ const WIDGETS = {};
684
+ const queued = (window.Sagadeck && window.Sagadeck._q) || [];
685
+ window.Sagadeck = {
686
+ widget(name, def) { WIDGETS[name] = def; },
687
+ store, toast, get slide() { return cur; }, get step() { return step; }, exportMode: EXPORT,
688
+ };
689
+ queued.forEach(([n, d]) => (WIDGETS[n] = d));
690
+ function mountWidgets() {
691
+ slides.forEach((s, si) => {
692
+ hooks[si].keys = [];
693
+ $$(".widget", s).forEach((w) => {
694
+ const def = WIDGETS[w.dataset.widget];
695
+ if (!def) { w.innerHTML = `<div style="padding:30px;border:3px dashed var(--em);color:var(--em);font:24px system-ui">widget "${w.dataset.widget}" não registrado</div>`; return; }
696
+ const opts = JSON.parse(w.dataset.opts || "{}");
697
+ const api = {
698
+ slide: si, exportMode: EXPORT, store, toast,
699
+ onEnter: (f) => hooks[si].enter.push(f), onLeave: (f) => hooks[si].leave.push(f),
700
+ onStep: (f) => hooks[si].step.push(f), onKey: (f) => hooks[si].keys.push(f),
701
+ get step() { return step; }, get current() { return cur === si; },
702
+ };
703
+ try { def.mount(w, opts, api); } catch (err) { console.error(err); w.textContent = "erro no widget: " + err.message; }
704
+ });
705
+ });
706
+ }
707
+
708
+ // ---------- apresentador ----------
709
+ let pvState = { i: 0, k: 0 }, t0 = null, paused = 0, pausedAt = null;
710
+ function presenterState(i, k) {
711
+ pvState = { i, k };
712
+ if (t0 == null && (i > 0 || k > 0)) t0 = Date.now();
713
+ const cl = (sel, idx, kk) => {
714
+ const box = $(sel), sc = $(".pv-scale", box);
715
+ sc.innerHTML = "";
716
+ if (idx >= N) { sc.innerHTML = `<div style="color:#777;font:600 60px system-ui;padding:80px">fim</div>`; return; }
717
+ const node = slides[idx].cloneNode(true); node.classList.add("current"); applyStatic(node, kk);
718
+ $$("[data-step]", node).forEach((e) => e.classList.toggle("in", kk >= +e.dataset.step));
719
+ sc.appendChild(node);
720
+ sc.style.transform = `scale(${box.clientWidth / 1920})`;
721
+ };
722
+ cl(".pv-cur", i, k);
723
+ const nextIdx = k < STEPS[i] ? i : i + 1, nextK = k < STEPS[i] ? k + 1 : 0;
724
+ cl(".pv-next", nextIdx, nextK);
725
+ $(".pv-nextlbl").textContent = k < STEPS[i] ? `Próximo clique (${k + 1}/${STEPS[i]})` : "Próximo slide";
726
+ const d = DATA.slides[i] || {};
727
+ $(".pv-notes").innerHTML = `<div class="pv-step">Slide ${i + 1}/${N}${STEPS[i] ? ` · clique ${k}/${STEPS[i]}` : ""}${d.time ? ` · ${d.time} min planejados` : ""}</div>` + (d.notes || "<p style='color:#777'>Sem notas.</p>");
728
+ $(".pv-title").textContent = d.title || "";
729
+ }
730
+ function presenterUI() {
731
+ document.body.classList.add("presenter");
732
+ const root = document.createElement("div"); root.id = "pv";
733
+ root.innerHTML = `<div class="pv-top"><span class="pv-clock">00:00</span><span class="pv-el">decorrido <b class="pv-elv">00:00</b> / ${DATA.duration || "?"}:00</span><span class="pv-pace">no ritmo</span><span class="pv-pace" style="background:#1e293b;color:#38bdf8;border:1px solid rgba(56,189,248,0.3);" title="Anti-Bloqueio corporativo ativo: a tela não será bloqueada por inatividade do Windows">Anti-bloqueio</span>
734
+ <button class="pv-start">Iniciar</button><button class="pv-reset">Zerar</button><button class="pv-black">Tela preta (B)</button><span class="pv-title"></span></div>
735
+ <div><div class="pv-lbl">Agora</div><div class="pv-cur pv-box" style="aspect-ratio:${SW}/${SH}"><div class="pv-scale"></div></div></div>
736
+ <div class="pv-side"><div><div class="pv-lbl pv-nextlbl">Próximo</div><div class="pv-next pv-box"><div class="pv-scale"></div></div></div><div class="pv-lbl">Notas / roteiro</div><div class="pv-notes"></div></div>`;
737
+ document.body.appendChild(root);
738
+ const fmt = (ms) => { const s = Math.floor(ms / 1000); return String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0"); };
739
+ setInterval(() => {
740
+ const now = new Date();
741
+ $(".pv-clock").textContent = now.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
742
+ const el = t0 == null ? 0 : (pausedAt || Date.now()) - t0 - paused;
743
+ $(".pv-elv").textContent = fmt(el);
744
+ const plan = DATA.slides.slice(0, pvState.i).reduce((a, s) => a + (s.time || 0), 0) * 60000;
745
+ const pace = $(".pv-pace"), diff = el - plan;
746
+ if (t0 == null) { pace.textContent = "aguardando início"; pace.className = "pv-pace"; }
747
+ else if (Math.abs(diff) < 60000) { pace.textContent = "no ritmo"; pace.className = "pv-pace ahead"; }
748
+ else { pace.textContent = (diff > 0 ? "atrasado " : "adiantado ") + fmt(Math.abs(diff)); pace.className = "pv-pace " + (diff > 0 ? "behind" : "ahead"); }
749
+ }, 500);
750
+ $(".pv-start").onclick = (e) => {
751
+ if (t0 == null) { t0 = Date.now(); e.target.textContent = "Pausar"; }
752
+ else if (pausedAt) { paused += Date.now() - pausedAt; pausedAt = null; e.target.textContent = "Pausar"; }
753
+ else { pausedAt = Date.now(); e.target.textContent = "Continuar"; }
754
+ };
755
+ $(".pv-reset").onclick = () => { t0 = null; paused = 0; pausedAt = null; $(".pv-start").textContent = "Iniciar"; };
756
+ $(".pv-black").onclick = () => cmd("blank");
757
+ window.addEventListener("resize", () => presenterState(pvState.i, pvState.k));
758
+ if (window.opener) window.opener.postMessage({ sagadeck: DATA.id, type: "hello" }, "*");
759
+ }
760
+
761
+ // ---------- Bloqueio de Suspensão / Anti-Lock corporativo ----------
762
+ let wakeLock = null;
763
+ let keepAwakeVideo = null;
764
+
765
+ async function requestWakeLock() {
766
+ if (EXPORT || window.parent!==window) return;
767
+ try {
768
+ if ("wakeLock" in navigator) {
769
+ wakeLock = await navigator.wakeLock.request("screen");
770
+ wakeLock.addEventListener("release", () => { wakeLock = null; });
771
+ return true;
772
+ }
773
+ } catch (err) {}
774
+
775
+ // Fallback: beacon de vídeo silencioso 1x1 inibe suspensão em navegadores corporativos
776
+ try {
777
+ if (!keepAwakeVideo) {
778
+ keepAwakeVideo = document.createElement("video");
779
+ keepAwakeVideo.setAttribute("playsinline", "");
780
+ keepAwakeVideo.setAttribute("muted", "");
781
+ keepAwakeVideo.setAttribute("loop", "");
782
+ keepAwakeVideo.style.cssText = "position:fixed;width:1px;height:1px;top:-10px;left:-10px;opacity:0.01;pointer-events:none;";
783
+ keepAwakeVideo.src = "data:video/webm;base64,GkXfo0AgQoaBAUL3gQDu4vqcgQdUaW5mb1ZAdYGAZW5jb2RpbmdlcHVibGlzaGVyX2FwcGxpY2F0aW9uY2hhcnNldAB4h5C5kIEYQoEB2QCQA4N1c2WDZkZlZmVmZmVmZmVmZmVmZmVmZmVmZmVmZg==";
784
+ document.body.appendChild(keepAwakeVideo);
785
+ keepAwakeVideo.play().catch(() => {});
786
+ }
787
+ } catch (e) {}
788
+ return false;
789
+ }
790
+
791
+ document.addEventListener("visibilitychange", () => {
792
+ if (document.visibilityState === "visible") requestWakeLock();
793
+ });
794
+
795
+ // ---------- inicialização ----------
796
+ function parseHash() {
797
+ const m = location.hash.match(/^#(\d+)(?:\.(\d+))?/);
798
+ return m ? [Math.max(0, +m[1] - 1), +(m[2] || 0)] : [0, 0];
799
+ }
800
+ function init() {
801
+ if (EXPORT) document.documentElement.classList.add("export");
802
+ scale(); window.addEventListener("resize", scale);
803
+ mountWidgets();
804
+ mountLessons();
805
+ initDrawing();
806
+ requestWakeLock();
807
+ document.addEventListener("keydown", onKey);
808
+ let mt; document.addEventListener("mousemove", (e) => {
809
+ document.body.classList.add("mouse"); clearTimeout(mt); mt = setTimeout(() => document.body.classList.remove("mouse"), 1600);
810
+ const l = $("#laser"); l.style.left = e.clientX + "px"; l.style.top = e.clientY + "px";
811
+ });
812
+ let tx = null; document.addEventListener("touchstart", (e) => (tx = e.touches[0].clientX));
813
+ document.addEventListener("touchend", (e) => { if (tx == null) return; const d = e.changedTouches[0].clientX - tx; if (Math.abs(d) > 50) cmd(d < 0 ? "next" : "prev"); tx = null; });
814
+ const [i, k] = parseHash();
815
+ slides.forEach((s) => s.classList.remove("current"));
816
+ cur = -1;
817
+ const start = () => { fitAll(); cur = 0; slides[0].classList.add("current"); goto(i, k, { force: true, instant: true }); };
818
+ (document.fonts ? document.fonts.ready : Promise.resolve()).then(start);
819
+ if (PRESENTER) presenterUI();
820
+ window.sagadeck = {
821
+ n: N, steps: (j) => STEPS[j], get cur() { return cur; }, get step() { return step; },
822
+ goto: (j, kk, all) => { goto(j, kk, { instant: true, force: true, all }); return new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); },
823
+ fit: fitAll, data: DATA, size: { w: SW, h: SH },
824
+ };
825
+ }
826
+ init();
827
+ })();