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,475 @@
1
+ // HTML -> PowerPoint EDITÁVEL.
2
+ // Lê a posição real de cada elemento no navegador e recria no .pptx:
3
+ // texto -> caixa de texto nativa (mesma fonte/tamanho/cor/espaçamento)
4
+ // caixas -> formas nativas (retângulo, arredondado, elipse)
5
+ // SVG/figuras/widgets -> imagem PNG 2x com fundo transparente
6
+ // gráficos (opcional --native-charts) -> gráfico nativo do PowerPoint
7
+ // cliques (data-step/data-exit) -> animações de entrada/saída no mesmo clique
8
+ // transição fade entre slides + notas do apresentador
9
+ import fs from "node:fs";
10
+ import PptxGenJS from "pptxgenjs";
11
+ import JSZip from "jszip";
12
+ import { openDeck } from "./browser.js";
13
+ import { pptxFontMap } from "../themes.js";
14
+ import { notesPlain } from "../markup.js";
15
+
16
+ const PX = 1 / 144; // 1920 px = 13,333 in
17
+ const PT = 0.5; // 1 px (na tela de 1920) = 0,5 pt
18
+
19
+ // ------------------------------------------------------------------ coleta no navegador
20
+ function collectInPage([idx, nativeCharts]) {
21
+ const slide = document.querySelectorAll("#stage > .slide")[idx];
22
+ document.querySelectorAll("[data-pid]").forEach((e) => e.removeAttribute("data-pid"));
23
+ const cv = document.createElement("canvas"); cv.width = cv.height = 1;
24
+ const cx = cv.getContext("2d", { willReadFrequently: true });
25
+ const rgba = (c) => {
26
+ if (!c || c === "transparent") return { hex: "000000", a: 0 };
27
+ cx.clearRect(0, 0, 1, 1); cx.fillStyle = "#000"; cx.fillStyle = c; cx.fillRect(0, 0, 1, 1);
28
+ const d = cx.getImageData(0, 0, 1, 1).data;
29
+ if (d[3] === 0) return { hex: "000000", a: 0 };
30
+ const un = (v) => Math.round(Math.min(255, (v * 255) / d[3]));
31
+ return { hex: [un(d[0]), un(d[1]), un(d[2])].map((v) => v.toString(16).padStart(2, "0")).join("").toUpperCase(), a: d[3] / 255 };
32
+ };
33
+ const sr = slide.getBoundingClientRect();
34
+ const box = (r) => ({ x: r.left - sr.left, y: r.top - sr.top, w: r.width, h: r.height });
35
+ const INLINE = new Set(["B", "I", "EM", "STRONG", "MARK", "SPAN", "A", "S", "CODE", "BR", "SUB", "SUP", "U", "SMALL"]);
36
+ let pid = 0;
37
+ const items = [];
38
+
39
+ const stepOf = (e) => {
40
+ let s = 0, ex = null;
41
+ for (let p = e; p && p !== slide; p = p.parentElement) {
42
+ if (p.dataset && p.dataset.step) s = Math.max(s, +p.dataset.step);
43
+ if (p.dataset && p.dataset.exit && ex == null) ex = +p.dataset.exit;
44
+ }
45
+ return { step: s, exit: ex };
46
+ };
47
+ const opac = (e) => { let o = 1; for (let p = e; p && p !== slide.parentElement; p = p.parentElement) o *= +getComputedStyle(p).opacity; return o; };
48
+ const isLeaf = (e) => {
49
+ const d = getComputedStyle(e).display;
50
+ if ((d.includes("flex") || d.includes("grid")) && e.children.length) return false;
51
+ let has = false;
52
+ for (const c of e.childNodes) {
53
+ if (c.nodeType === 3) { if (c.textContent.trim()) has = true; }
54
+ else if (c.nodeType === 1) {
55
+ if (!INLINE.has(c.tagName)) return false;
56
+ if (c.querySelector && c.querySelector("svg,img,div,canvas")) return false;
57
+ if (c.textContent.trim()) has = true;
58
+ }
59
+ }
60
+ return has;
61
+ };
62
+ const transformed = (cs) => (cs.transform && cs.transform !== "none" && !/^matrix\(1, 0, 0, 1,/.test(cs.transform)) || (cs.writingMode && cs.writingMode !== "horizontal-tb");
63
+ // área visual real de um SVG (desenhos podem "vazar" da caixa com overflow:visible)
64
+ const visualRect = (e) => {
65
+ let L = Infinity, T = Infinity, Rr = -Infinity, B = -Infinity;
66
+ const add = (r) => { if (r.width || r.height) { L = Math.min(L, r.left); T = Math.min(T, r.top); Rr = Math.max(Rr, r.right); B = Math.max(B, r.bottom); } };
67
+ add(e.getBoundingClientRect());
68
+ if (e.tagName.toLowerCase() === "svg") e.querySelectorAll("*").forEach((c) => { if (c.getBoundingClientRect && !["defs", "marker", "clippath", "lineargradient", "filter"].includes(c.tagName.toLowerCase()) && !c.closest("defs")) add(c.getBoundingClientRect()); });
69
+ L = Math.max(L, sr.left); T = Math.max(T, sr.top); Rr = Math.min(Rr, sr.right); B = Math.min(B, sr.bottom);
70
+ return { left: L, top: T, width: Rr - L, height: B - T, right: Rr, bottom: B };
71
+ };
72
+
73
+ // Texto em "runs", com as MESMAS quebras de linha que o navegador fez (palavra a palavra).
74
+ function runsOf(leaf, lh) {
75
+ const paras = [[]];
76
+ const baseW = +getComputedStyle(leaf).fontWeight;
77
+ const baseItalic = getComputedStyle(leaf).fontStyle === "italic";
78
+ const rng = document.createRange();
79
+ let lastTop = null, pendingSpace = false;
80
+ const styleOf = (el) => {
81
+ const cs = getComputedStyle(el);
82
+ const faceEl = el.closest('[class*="f-"]');
83
+ const role = faceEl ? ([...faceEl.classList].find((k) => /^f-(display|heading|body|label|mono|quote)$/.test(k)) || "").slice(2) : "";
84
+ // link para outro slide (goto, [texto](#id), Voltar): vira "slide:N" e, no PowerPoint, salto para o slide
85
+ const go = el.closest("[data-goto]");
86
+ const link = go ? null : el.closest("a");
87
+ const goHref = go ? (() => { const all = [...document.querySelectorAll("#stage > .slide")], t = String(go.dataset.goto), j = all.findIndex((x) => x.dataset.id === t); const n = j >= 0 ? j : /^\d+$/.test(t) ? +t - 1 : -1; return n >= 0 ? `slide:${n}` : null; })() : null;
88
+ const col = rgba(cs.color);
89
+ return {
90
+ color: col.hex, alpha: col.a, size: parseFloat(cs.fontSize), weight: +cs.fontWeight, baseW,
91
+ italic: cs.fontStyle === "italic", baseItalic, family: cs.fontFamily.split(",")[0].replace(/["']/g, "").trim(), role,
92
+ spacing: parseFloat(cs.letterSpacing) || 0, upper: cs.textTransform === "uppercase", stretch: parseFloat(cs.fontStretch) || 100,
93
+ strike: cs.textDecorationLine.includes("line-through"), underline: !link && cs.textDecorationLine.includes("underline"),
94
+ href: goHref || (link ? link.href : null),
95
+ };
96
+ };
97
+ const same = (a, b) => ["color", "alpha", "size", "weight", "italic", "family", "role", "spacing", "strike", "underline", "href"].every((k) => a[k] === b[k]);
98
+ const push = (txt, st, newLine) => {
99
+ if (newLine && paras[paras.length - 1].length) paras.push([]);
100
+ const P = paras[paras.length - 1], prev = P[P.length - 1];
101
+ if (prev && same(prev, st)) prev.text += txt; else P.push({ ...st, text: txt });
102
+ };
103
+ const walk = (n) => {
104
+ for (const c of n.childNodes) {
105
+ if (c.nodeType === 3) {
106
+ const raw = c.textContent, st = styleOf(c.parentElement);
107
+ const re = /\S+/g; let m, lastEnd = 0;
108
+ while ((m = re.exec(raw))) {
109
+ if (m.index > lastEnd) pendingSpace = true;
110
+ rng.setStart(c, m.index); rng.setEnd(c, m.index + m[0].length);
111
+ const rects = rng.getClientRects();
112
+ const top = rects.length ? rects[0].top : lastTop;
113
+ const newLine = lastTop != null && top != null && top > lastTop + lh * 0.5;
114
+ let word = st.upper ? m[0].toUpperCase() : m[0];
115
+ const lineHasText = paras[paras.length - 1].length && !newLine;
116
+ push((pendingSpace && lineHasText ? " " : "") + word, st, newLine);
117
+ pendingSpace = false; lastTop = top; lastEnd = m.index + m[0].length;
118
+ }
119
+ if (raw.length > lastEnd && /\s/.test(raw.slice(lastEnd))) pendingSpace = true;
120
+ } else if (c.nodeType === 1) {
121
+ if (c.tagName === "BR") { paras.push([]); lastTop = null; pendingSpace = false; continue; }
122
+ const blk = getComputedStyle(c).display === "block";
123
+ if (blk && paras[paras.length - 1].length) { paras.push([]); lastTop = null; pendingSpace = false; }
124
+ walk(c);
125
+ if (blk) { paras.push([]); lastTop = null; pendingSpace = false; }
126
+ }
127
+ }
128
+ };
129
+ walk(leaf);
130
+ return paras.filter((p) => p.length);
131
+ }
132
+
133
+ function visit(e) {
134
+ const cs = getComputedStyle(e);
135
+ if (cs.display === "none" || cs.visibility === "hidden" || +cs.opacity === 0) return;
136
+ const r = e.getBoundingClientRect();
137
+ if (r.width < 1 || r.height < 1) { if (e.children.length) [...e.children].forEach(visit); return; }
138
+ const tag = e.tagName;
139
+ const st = stepOf(e);
140
+ const tagIt = (o) => { e.setAttribute("data-pid", String(++pid)); items.push({ pid, ...box(r), ...st, opacity: opac(e), ...o }); };
141
+
142
+ // gráfico nativo
143
+ if (nativeCharts && tag.toLowerCase() === "svg" && e.classList.contains("chart")) {
144
+ const spec = JSON.parse(e.dataset.chart || "{}");
145
+ if (["bar", "column", "line", "donut"].includes(spec.type)) {
146
+ const sel = spec.type === "line" ? ".draw" : spec.type === "donut" ? ".arc" : spec.type === "bar" ? ".gx" : ".gy";
147
+ const colors = [...e.querySelectorAll(sel)].map((n) => rgba(getComputedStyle(n)[spec.type === "line" || spec.type === "donut" ? "stroke" : "fill"]).hex);
148
+ const fg = rgba(getComputedStyle(slide).color).hex;
149
+ tagIt({ kind: "chart", spec, colors, fg });
150
+ return;
151
+ }
152
+ }
153
+ // tabela de verdade (src/table.js): tabela nativa do PowerPoint, editável, com as cores e larguras da tela
154
+ if (tag === "TABLE" && e.classList.contains("dtable")) {
155
+ const trs = [...e.rows];
156
+ const rows = trs.map((tr) => [...tr.cells].map((c) => {
157
+ const ccs = getComputedStyle(c);
158
+ let bg = rgba(ccs.backgroundColor);
159
+ if (bg.a < 0.01) bg = rgba(getComputedStyle(tr).backgroundColor);
160
+ const col = rgba(ccs.color), bb = rgba(ccs.borderBottomColor);
161
+ const run = { family: ccs.fontFamily.split(",")[0].replace(/["']/g, "").trim(), weight: +ccs.fontWeight, baseW: 400, italic: ccs.fontStyle === "italic", baseItalic: false, role: "body", stretch: 100 };
162
+ return { text: c.innerText.trim(), run, color: col.hex, size: parseFloat(ccs.fontSize), fill: bg.a > 0.01 ? bg.hex : null, fillA: bg.a, align: ccs.textAlign === "right" || ccs.textAlign === "end" ? "right" : ccs.textAlign === "center" ? "center" : "left",
163
+ pad: [ccs.paddingTop, ccs.paddingRight, ccs.paddingBottom, ccs.paddingLeft].map(parseFloat), line: parseFloat(ccs.borderBottomWidth) > 0 && bb.a > 0.05 ? { color: bb.hex, w: parseFloat(ccs.borderBottomWidth) } : null };
164
+ }));
165
+ const colW = trs.length ? [...trs[0].cells].map((c) => c.getBoundingClientRect().width) : [];
166
+ const rowH = trs.map((tr) => tr.getBoundingClientRect().height);
167
+ tagIt({ kind: "table", rows, colW, rowH });
168
+ return;
169
+ }
170
+ // coisas que viram imagem
171
+ if (tag.toLowerCase() === "svg" || tag === "IMG" || tag === "CANVAS" || tag === "VIDEO" ||
172
+ e.classList.contains("katex") || (isLeaf(e) && e.querySelector(".katex")) || e.classList.contains("widget") || e.classList.contains("timer") || e.classList.contains("code") || e.classList.contains("raster") || transformed(cs)) {
173
+ const vr = visualRect(e);
174
+ e.setAttribute("data-pid", String(++pid));
175
+ items.push({ pid, kind: "image", ...box(vr), clip: { x: vr.left, y: vr.top, width: vr.width, height: vr.height }, ...st, opacity: opac(e) });
176
+ return;
177
+ }
178
+ // caixa (fundo/borda)
179
+ const bg = rgba(cs.backgroundColor);
180
+ const bw = parseFloat(cs.borderTopWidth) || 0;
181
+ const sameBorder = ["Right", "Bottom", "Left"].every((s) => cs[`border${s}Width`] === cs.borderTopWidth && cs[`border${s}Color`] === cs.borderTopColor);
182
+ const bc = rgba(cs.borderTopColor);
183
+ const hasBorder = bw > 0 && bc.a > 0.01 && cs.borderTopStyle !== "none" && sameBorder;
184
+ if (tag !== "MARK" && cs.backgroundImage !== "none" && !cs.backgroundImage.startsWith("linear-gradient(rgb") ) {
185
+ tagIt({ kind: "image", bgOnly: true });
186
+ } else if (bg.a > 0.01 || hasBorder) {
187
+ const rad = cs.borderTopLeftRadius;
188
+ const rpx = rad.endsWith("%") ? (parseFloat(rad) / 100) * Math.min(r.width, r.height) : parseFloat(rad) || 0;
189
+ const shape = rpx >= Math.min(r.width, r.height) / 2 - 0.5 ? (Math.abs(r.width - r.height) < 2 ? "ellipse" : "pill") : rpx > 0 ? "round" : "rect";
190
+ e.setAttribute("data-pid", String(++pid));
191
+ items.push({ pid, kind: "shape", ...box(r), ...st, opacity: opac(e), shape, radius: rpx, fill: bg.a > 0.01 ? bg.hex : null, fillA: bg.a, line: hasBorder ? bc.hex : null, lineA: bc.a, lineW: hasBorder ? bw : 0 });
192
+ }
193
+ // bordas diferentes por lado (divisor embaixo, barra à esquerda…): cada lado vira uma linha fina nativa
194
+ if (!sameBorder) {
195
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
196
+ const w = parseFloat(cs[`border${side}Width`]) || 0, c = rgba(cs[`border${side}Color`]);
197
+ if (w <= 0 || c.a <= 0.01 || cs[`border${side}Style`] === "none") continue;
198
+ const edge = side === "Top" ? { left: r.left, top: r.top, width: r.width, height: w }
199
+ : side === "Bottom" ? { left: r.left, top: r.bottom - w, width: r.width, height: w }
200
+ : side === "Left" ? { left: r.left, top: r.top, width: w, height: r.height }
201
+ : { left: r.right - w, top: r.top, width: w, height: r.height };
202
+ items.push({ pid: ++pid, kind: "shape", ...box(edge), ...st, opacity: opac(e), shape: "rect", radius: 0, fill: c.hex, fillA: c.a, line: null, lineA: 0, lineW: 0 });
203
+ }
204
+ }
205
+ // texto
206
+ if (isLeaf(e)) {
207
+ const pl = parseFloat(cs.paddingLeft) + parseFloat(cs.borderLeftWidth || 0), pr = parseFloat(cs.paddingRight) + parseFloat(cs.borderRightWidth || 0);
208
+ const pt = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth || 0), pb = parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth || 0);
209
+ const b = box(r);
210
+ const flexCenter = cs.display.includes("flex") && cs.alignItems === "center";
211
+ const ta = cs.textAlign === "start" || cs.textAlign === "left" ? "left" : cs.textAlign === "end" ? "right" : cs.textAlign === "justify" ? "left" : cs.textAlign;
212
+ const align = cs.display.includes("flex") && cs.justifyContent === "center" ? "center" : ta;
213
+ const lh = cs.lineHeight === "normal" ? parseFloat(cs.fontSize) * 1.2 : parseFloat(cs.lineHeight);
214
+ // marca-texto: um retângulo por linha, atrás do texto
215
+ e.querySelectorAll("mark").forEach((m) => {
216
+ const mcs = getComputedStyle(m);
217
+ if (!m.classList.contains("play") && !document.documentElement.classList.contains("export")) return;
218
+ const hi = rgba(mcs.getPropertyValue("--hi").trim() || mcs.backgroundColor);
219
+ for (const cr of m.getClientRects()) {
220
+ const bh = cr.height * 0.92, by = cr.top + cr.height * 0.044;
221
+ items.push({ pid: ++pid, kind: "shape", x: cr.left - sr.left, y: by - sr.top, w: cr.width, h: bh, ...st, opacity: opac(e), shape: "rect", radius: 0, fill: hi.hex, fillA: hi.a, line: null, lineA: 0, lineW: 0 });
222
+ }
223
+ });
224
+ items.push({
225
+ pid: ++pid, kind: "text", x: b.x + pl, y: b.y + pt, w: b.w - pl - pr, h: b.h - pt - pb, ...st, opacity: opac(e),
226
+ align, valign: flexCenter ? "middle" : "top", lineHeight: lh, paras: runsOf(e, lh), nowrap: true,
227
+ });
228
+ return;
229
+ }
230
+ [...e.children].forEach(visit);
231
+ }
232
+ [...slide.children].forEach(visit);
233
+ const sbg = rgba(getComputedStyle(slide).backgroundColor).hex;
234
+ return { bg: sbg, tr: slide.dataset.tr || "fade", items };
235
+ }
236
+
237
+ // ------------------------------------------------------------------ fontes
238
+ // Fontes variáveis do Windows: (peso, largura) -> nome exato da instância no PowerPoint
239
+ const VARIABLE = {
240
+ Bahnschrift: {
241
+ weights: [300, 350, 400, 600, 700], widths: [75, 87.5, 100],
242
+ name: (w, x) => {
243
+ const wn = { 300: " Light", 350: " SemiLight", 400: "", 600: " SemiBold", 700: "" }[w];
244
+ const xn = x === 75 ? " Condensed" : x === 87.5 ? (w === 600 ? " SemiConden" : w === 350 ? " SemiConde" : " SemiCondensed") : "";
245
+ return { face: `Bahnschrift${wn}${xn}`, bold: w === 700 };
246
+ },
247
+ },
248
+ };
249
+ const near = (arr, v) => arr.reduce((a, b) => (Math.abs(b - v) < Math.abs(a - v) ? b : a));
250
+ function fontFor(run, fmap, aliases = {}) {
251
+ const fam = aliases[run.family] || run.family;
252
+ if (VARIABLE[fam]) {
253
+ const V = VARIABLE[fam];
254
+ const r = V.name(near(V.weights, run.weight), near(V.widths, run.stretch || 100));
255
+ return { face: r.face, bold: r.bold, italic: run.italic };
256
+ }
257
+ const m = fmap[run.role];
258
+ let face = fam, bold = run.weight >= 600, italic = run.italic;
259
+ if (m) {
260
+ const extraBold = run.weight >= Math.max(600, run.baseW + 150);
261
+ const f = extraBold ? m.bold : m.regular;
262
+ face = f.face; bold = !!f.bold; italic = !!f.italic || (run.italic && !run.baseItalic);
263
+ }
264
+ return { face, bold, italic };
265
+ }
266
+
267
+ // ------------------------------------------------------------------ animações
268
+ function timingXML(anims, ids) {
269
+ // anims: [{spid, step, exit, isSp}]
270
+ let id = 2;
271
+ const clicks = new Map();
272
+ for (const a of anims) {
273
+ if (a.step > 0) { if (!clicks.has(a.step)) clicks.set(a.step, []); clicks.get(a.step).push({ ...a, type: "in" }); }
274
+ if (a.exit != null && a.exit > 0) { if (!clicks.has(a.exit)) clicks.set(a.exit, []); clicks.get(a.exit).push({ ...a, type: "out" }); }
275
+ }
276
+ if (!clicks.size) return "";
277
+ const keys = [...clicks.keys()].sort((a, b) => a - b);
278
+ let seq = "";
279
+ const bld = new Set();
280
+ for (const k of keys) {
281
+ const list = clicks.get(k);
282
+ let inner = "";
283
+ list.forEach((a, j) => {
284
+ const node = j === 0 ? "clickEffect" : "withEffect";
285
+ const grp = a.type === "in" ? 0 : 1;
286
+ if (a.isSp) bld.add(`<p:bldP spid="${a.spid}" grpId="${grp}" animBg="1"/>`);
287
+ if (a.type === "in") {
288
+ inner += `<p:par><p:cTn id="${++id}" presetID="10" presetClass="entr" presetSubtype="0" fill="hold" grpId="${grp}" nodeType="${node}"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>` +
289
+ `<p:set><p:cBhvr><p:cTn id="${++id}" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible"/></p:to></p:set>` +
290
+ `<p:animEffect transition="in" filter="fade"><p:cBhvr><p:cTn id="${++id}" dur="500"/><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl></p:cBhvr></p:animEffect>` +
291
+ `</p:childTnLst></p:cTn></p:par>`;
292
+ } else {
293
+ inner += `<p:par><p:cTn id="${++id}" presetID="10" presetClass="exit" presetSubtype="0" fill="hold" grpId="${grp}" nodeType="${node}"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>` +
294
+ `<p:animEffect transition="out" filter="fade"><p:cBhvr><p:cTn id="${++id}" dur="400"/><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl></p:cBhvr></p:animEffect>` +
295
+ `<p:set><p:cBhvr><p:cTn id="${++id}" dur="1" fill="hold"><p:stCondLst><p:cond delay="399"/></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="hidden"/></p:to></p:set>` +
296
+ `</p:childTnLst></p:cTn></p:par>`;
297
+ }
298
+ });
299
+ seq += `<p:par><p:cTn id="${++id}" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst><p:par><p:cTn id="${++id}" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>${inner}</p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par>`;
300
+ }
301
+ const initial = anims.filter(a => a.step > 0).map(a => `<p:set><p:cBhvr><p:cTn id="${++id}" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="hidden"/></p:to></p:set>`).join("");
302
+ return `<p:timing><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst>${initial}<p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst>${seq}</p:childTnLst></p:cTn><p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst><p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>${bld.size ? `<p:bldLst>${[...bld].join("")}</p:bldLst>` : ""}</p:timing>`;
303
+ }
304
+
305
+ // ------------------------------------------------------------------ principal
306
+ // Linhas e decorações feitas com ::before/::after (eixo da linha do tempo, conectores, marcadores) não
307
+ // existem no DOM: viram <span> de verdade com o mesmo estilo calculado, e o coletor passa a enxergá-las.
308
+ function materializePseudos() {
309
+ for (const e of document.querySelectorAll("#stage > .slide *")) {
310
+ for (const which of ["::before", "::after"]) {
311
+ const cs = getComputedStyle(e, which);
312
+ if (!cs || cs.display === "none" || !/^["']/.test(cs.content || "")) continue; // só content: "…" (desenho ou texto)
313
+ const span = document.createElement("span");
314
+ // copia o estilo calculado, menos o que depende de qual slide está na tela agora (visibilidade) e animações
315
+ for (const prop of cs) if (!/^(visibility|transition|animation)/.test(prop)) span.style.setProperty(prop, cs.getPropertyValue(prop));
316
+ const text = cs.content.slice(1, -1);
317
+ if (text) span.textContent = text;
318
+ span.dataset.pseudo = which;
319
+ if (which === "::before") e.prepend(span); else e.append(span);
320
+ }
321
+ }
322
+ const st = document.createElement("style");
323
+ st.textContent = "#stage > .slide *::before, #stage > .slide *::after { content: none !important; }";
324
+ document.head.append(st);
325
+ }
326
+
327
+ // notes: false tira as notas do apresentador (mandar a apresentação para alguém sem a "cola")
328
+ export async function exportPptx(htmlFile, outFile, { theme, meta, nativeCharts = false, notes = true, log = () => {} } = {}) {
329
+ const notesOpt = notes !== false;
330
+ const { browser, page, errors } = await openDeck(htmlFile, { scale: 2 });
331
+ try {
332
+ await page.evaluate(materializePseudos);
333
+ await page.addStyleTag({ content: `
334
+ html.solo,html.solo body,html.solo #viewport,html.solo .slide{background:transparent!important}
335
+ html.solo .slide::after,html.solo .slide::before{display:none!important}
336
+ html.solo *{visibility:hidden!important}
337
+ html.solo .solo-t,html.solo .solo-t *{visibility:visible!important}
338
+ html.solo .solo-t.solo-bg *{visibility:hidden!important}` });
339
+ const fmap = pptxFontMap(theme);
340
+ // apelidos CSS (ex.: SagaDIN -> Bahnschrift) declarados em fontFaces do tema
341
+ const aliases = Object.fromEntries((theme.fontFaces || []).map((f) => [f.family, (f.src.match(/local\(['"]?([^'")]+)/) || [])[1]]).filter((x) => x[1]));
342
+ const pres = new PptxGenJS();
343
+ const size = await page.evaluate(() => window.sagadeck.size || { w: 1920, h: 1080 });
344
+ if (size.h === 1080) pres.layout = "LAYOUT_WIDE";
345
+ else { pres.defineLayout({ name: "SAGADECK", width: 13.333, height: +(13.333 * size.h / size.w).toFixed(3) }); pres.layout = "SAGADECK"; } // deck.aspect
346
+ pres.title = meta.title || "";
347
+ pres.author = meta.author || "";
348
+ const n = await page.evaluate(() => window.sagadeck.n);
349
+ const animBySlide = [];
350
+
351
+ for (let i = 0; i < n; i++) {
352
+ const S = await page.evaluate((j) => window.sagadeck.steps(j), i);
353
+ await page.evaluate(([j, k]) => window.sagadeck.goto(j, k, true), [i, S]);
354
+ await page.waitForTimeout(40);
355
+ const data = await page.evaluate(collectInPage, [i, nativeCharts]);
356
+ const slide = pres.addSlide();
357
+ slide.background = { color: data.bg };
358
+ const anims = [];
359
+ for (const it of data.items) {
360
+ const name = `sagadeck-${it.pid}`;
361
+ const geo = { x: it.x * PX, y: it.y * PX, w: Math.max(it.w, 1) * PX, h: Math.max(it.h, 1) * PX };
362
+ const alpha = it.opacity ?? 1;
363
+ if (it.kind === "shape") {
364
+ const shape = it.shape === "ellipse" ? pres.ShapeType.ellipse : it.shape === "rect" ? pres.ShapeType.rect : pres.ShapeType.roundRect;
365
+ const o = { ...geo, objectName: name };
366
+ o.fill = it.fill ? { color: it.fill, transparency: Math.round((1 - it.fillA * alpha) * 100) } : { type: "none" };
367
+ o.line = it.line ? { color: it.line, width: it.lineW * PT, transparency: Math.round((1 - it.lineA * alpha) * 100) } : { type: "none" };
368
+ if (shape === pres.ShapeType.roundRect) o.rectRadius = Math.min(it.radius, Math.min(it.w, it.h) / 2) * PX;
369
+ slide.addShape(shape, o);
370
+ anims.push({ name, step: it.step, exit: it.exit, isSp: true });
371
+ } else if (it.kind === "text") {
372
+ const runs = [];
373
+ it.paras.forEach((p, pi) => p.forEach((r, ri) => {
374
+ const f = fontFor(r, fmap, aliases);
375
+ const o = { fontFace: f.face, fontSize: +(r.size * PT).toFixed(2), bold: f.bold, italic: f.italic, color: r.color };
376
+ const tr = Math.round((1 - r.alpha * alpha) * 100); if (tr > 0) o.transparency = tr;
377
+ if (r.spacing) o.charSpacing = +(r.spacing * PT).toFixed(2);
378
+ if (r.hi) o.highlight = r.hi;
379
+ if (r.strike) o.strike = "sngStrike";
380
+ if (r.underline) o.underline = { style: "sng" };
381
+ if (r.href) o.hyperlink = r.href.startsWith("slide:") ? { slide: +r.href.slice(6) + 1 } : { url: r.href };
382
+ if (ri === p.length - 1 && pi < it.paras.length - 1) o.breakLine = true;
383
+ runs.push({ text: r.text, options: o });
384
+ }));
385
+ if (!runs.length) continue;
386
+ // folga para diferenças mínimas de métrica entre navegador e PowerPoint
387
+ const slack = it.nowrap ? 0.12 : 0.01;
388
+ const extra = it.w * slack;
389
+ let x = it.x - (it.align === "center" ? extra / 2 : it.align === "right" ? extra : 0);
390
+ slide.addText(runs, {
391
+ x: x * PX, y: it.y * PX, w: (it.w + extra) * PX, h: Math.max(it.h, it.lineHeight) * PX,
392
+ margin: 0, align: it.align, valign: it.valign, lineSpacing: +(it.lineHeight * PT).toFixed(2),
393
+ paraSpaceBefore: 0, paraSpaceAfter: 0, fit: "none", wrap: !it.nowrap, isTextBox: true, objectName: name,
394
+ });
395
+ anims.push({ name, step: it.step, exit: it.exit, isSp: true });
396
+ } else if (it.kind === "image") {
397
+ const loc = page.locator(`[data-pid="${it.pid}"]`);
398
+ await page.evaluate(([pid, bgOnly]) => {
399
+ document.documentElement.classList.add("solo");
400
+ const el = document.querySelector(`[data-pid="${pid}"]`);
401
+ el.classList.add("solo-t"); if (bgOnly) el.classList.add("solo-bg");
402
+ }, [it.pid, !!it.bgOnly]);
403
+ const buf = it.clip
404
+ ? await page.screenshot({ omitBackground: true, animations: "disabled", clip: it.clip })
405
+ : await loc.screenshot({ omitBackground: true, animations: "disabled" });
406
+ await page.evaluate((pid) => {
407
+ document.documentElement.classList.remove("solo");
408
+ const el = document.querySelector(`[data-pid="${pid}"]`); el.classList.remove("solo-t", "solo-bg");
409
+ }, it.pid);
410
+ slide.addImage({ data: "image/png;base64," + buf.toString("base64"), ...geo, objectName: name });
411
+ anims.push({ name, step: it.step, exit: it.exit, isSp: false });
412
+ } else if (it.kind === "table") {
413
+ const rows = it.rows.map((r) => r.map((c) => {
414
+ const f = fontFor(c.run, fmap, aliases);
415
+ const o = { fontFace: f.face, bold: f.bold, italic: f.italic, fontSize: +(c.size * PT).toFixed(2), color: c.color, align: c.align, valign: "middle", margin: c.pad.map((v) => +(v * PT).toFixed(2)) };
416
+ o.fill = c.fill ? { color: c.fill, transparency: Math.round((1 - c.fillA * alpha) * 100) } : { type: "none" };
417
+ const none = { type: "none" };
418
+ o.border = [none, none, c.line ? { type: "solid", color: c.line.color, pt: +(c.line.w * PT).toFixed(2) } : none, none];
419
+ return { text: c.text, options: o };
420
+ }));
421
+ slide.addTable(rows, { ...geo, colW: it.colW.map((w) => w * PX), rowH: it.rowH.map((h) => h * PX), objectName: name, autoPage: false });
422
+ anims.push({ name, step: it.step, exit: it.exit, isSp: false });
423
+ } else if (it.kind === "chart") {
424
+ addNativeChart(pres, slide, it, geo, name, fmap);
425
+ anims.push({ name, step: it.step, exit: it.exit, isSp: false });
426
+ }
427
+ }
428
+ const notes = meta.slides[i]?.notesRaw ?? "", withNotes = notesOpt;
429
+ if (notes && withNotes) slide.addNotes(notesPlain(notes));
430
+ animBySlide.push({ anims, tr: data.tr });
431
+ log(` slide ${i + 1}/${n}: ${data.items.length} objetos`);
432
+ }
433
+ // pós-processamento: animações + transições
434
+ const buf = await pres.write({ outputType: "nodebuffer" });
435
+ const zip = await JSZip.loadAsync(buf);
436
+ for (let i = 0; i < animBySlide.length; i++) {
437
+ const f = `ppt/slides/slide${i + 1}.xml`;
438
+ let xml = await zip.file(f).async("string");
439
+ const ids = {};
440
+ for (const m of xml.matchAll(/<p:cNvPr id="(\d+)" name="(sagadeck-\d+)"/g)) ids[m[2]] = m[1];
441
+ const anims = animBySlide[i].anims.filter((a) => ids[a.name] && (a.step > 0 || a.exit != null)).map((a) => ({ ...a, spid: ids[a.name] }));
442
+ const tr = animBySlide[i].tr === "cut" ? "" : `<p:transition spd="med"><p:fade/></p:transition>`;
443
+ const timing = timingXML(anims);
444
+ xml = xml.replace(/<\/p:clrMapOvr>/, `</p:clrMapOvr>${tr}${timing}`);
445
+ if (!/<\/p:clrMapOvr>/.test(xml)) xml = xml.replace(/<\/p:sld>/, `${tr}${timing}</p:sld>`);
446
+ zip.file(f, xml);
447
+ }
448
+ const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
449
+ fs.writeFileSync(outFile, out);
450
+ return { errors };
451
+ } finally { await browser.close(); }
452
+ }
453
+
454
+ function addNativeChart(pres, slide, it, geo, name, fmap) {
455
+ const s = it.spec, font = fmap.body?.regular?.face || "Arial";
456
+ const base = { ...geo, objectName: name, fontFace: font, catAxisLabelColor: it.fg, valAxisLabelColor: it.fg, dataLabelColor: it.fg, showLegend: false,
457
+ valGridLine: { style: "none" }, catGridLine: { style: "none" }, catAxisLabelFontSize: 16, dataLabelFontSize: 20, dataLabelFontFace: font, catAxisLabelFontFace: font };
458
+ if (s.type === "bar" || s.type === "column") {
459
+ const data = s.series ? s.series.map((x) => ({ name: x.name, labels: s.labels, values: x.values.map((v) => (v == null ? "" : v)) })) : [{ name: "série", labels: s.labels, values: s.values }];
460
+ slide.addChart(pres.ChartType.bar, data, {
461
+ ...base, barDir: s.type === "bar" ? "bar" : "col", chartColors: it.colors.length ? it.colors : ["888888"], barGapWidthPct: 50, barGrouping: "clustered", showLegend: !!s.series, legendPos: "t",
462
+ showValue: true, dataLabelPosition: "outEnd", dataLabelFormatCode: `"${s.prefix || ""}"0"${s.suffix || ""}"`, valAxisHidden: true,
463
+ valAxisMaxVal: s.max, catAxisOrientation: s.type === "bar" ? "maxMin" : "minMax", catAxisLineShow: false,
464
+ });
465
+ } else if (s.type === "line") {
466
+ slide.addChart(pres.ChartType.line, s.series.map((x) => ({ name: x.name, labels: s.labels, values: x.values.map((v) => (v == null ? "" : v)) })), {
467
+ ...base, chartColors: it.colors.length ? it.colors : undefined, lineSize: 3, lineDataSymbolSize: 6, valAxisMinVal: s.min, valAxisMaxVal: s.max,
468
+ valGridLine: { color: "DDDDDD", size: 0.5 }, showLegend: s.series.length > 1, legendPos: "b",
469
+ });
470
+ } else if (s.type === "donut") {
471
+ slide.addChart(pres.ChartType.doughnut, [{ name: "série", labels: s.parts.map((p) => p.label), values: s.parts.map((p) => p.value) }], {
472
+ ...base, chartColors: it.colors, holeSize: 62, showValue: false, showPercent: false,
473
+ });
474
+ }
475
+ }
@@ -0,0 +1,40 @@
1
+ // Roteiro do apresentador em PDF: miniatura de cada slide + notas + relógio planejado.
2
+ import fs from "node:fs";
3
+ import { chromium } from "playwright-core";
4
+ import { findBrowser } from "./browser.js";
5
+ import { esc } from "../markup.js";
6
+
7
+ export async function roteiroPDF({ meta, slidesMeta, shotFiles, outFile, title, author, duration }) {
8
+ let acc = 0;
9
+ const fmt = (m) => `${String(Math.floor(m)).padStart(2, "0")}:${String(Math.round((m % 1) * 60)).padStart(2, "0")}`;
10
+ const rows = slidesMeta.map((s, i) => {
11
+ const start = acc; acc += s.time || 0;
12
+ const img = shotFiles[i] ? `<img src="data:image/${shotFiles[i].endsWith(".jpg") ? "jpeg" : "png"};base64,${fs.readFileSync(shotFiles[i]).toString("base64")}">` : "";
13
+ return `<section><div class="l"><div class="n">${String(i + 1).padStart(2, "0")}</div>${img}<div class="tm">${s.time ? `${fmt(start)} → ${fmt(acc)} · ${s.time} min` : ""}</div></div>
14
+ <div class="r"><h3>${esc(s.title)}</h3>${s.notes || "<p class='muted'>—</p>"}</div></section>`;
15
+ }).join("");
16
+ const html = `<!doctype html><html><head><meta charset="utf-8"><style>
17
+ @page{size:A4;margin:14mm 14mm 16mm}
18
+ body{font:11pt/1.5 'Segoe UI',Arial,sans-serif;color:#1a1a1a;margin:0}
19
+ header{border-bottom:2px solid #111;padding-bottom:8px;margin-bottom:14px}
20
+ header h1{font:700 22pt/1.1 'Segoe UI';margin:0}
21
+ header p{margin:4px 0 0;color:#666}
22
+ section{display:grid;grid-template-columns:62mm 1fr;gap:6mm;padding:5mm 0;border-bottom:1px solid #ddd;break-inside:avoid}
23
+ .l img{width:100%;border-radius:3px;border:1px solid #ccc;display:block}
24
+ .n{font:700 10pt 'Segoe UI';color:#999;margin-bottom:2px}
25
+ .tm{font:600 9pt 'Segoe UI';color:#b3261e;margin-top:4px}
26
+ h3{font:700 12.5pt/1.25 'Segoe UI';margin:0 0 6px}
27
+ p{margin:0 0 6px}
28
+ .say{border-left:3px solid #f2b705;padding-left:8px;font-size:11.5pt}
29
+ .tag{display:inline-block;font:700 7.5pt 'Segoe UI';letter-spacing:.1em;background:#111;color:#ffc20e;padding:1px 6px;border-radius:3px;margin-right:6px;vertical-align:1px}
30
+ ul{margin:0 0 6px;padding-left:16px}
31
+ h4{font:700 8.5pt 'Segoe UI';letter-spacing:.12em;text-transform:uppercase;color:#777;margin:8px 0 3px}
32
+ mark{background:#ffe07a}
33
+ .muted{color:#999}
34
+ </style></head><body><header><h1>Roteiro · ${esc(title || "")}</h1><p>${esc(author || "")}${duration ? ` · ${duration} min` : ""} · tempo planejado: ${Math.round(acc)} min · ${slidesMeta.length} slides</p></header>${rows}</body></html>`;
35
+ const browser = await chromium.launch({ executablePath: findBrowser() });
36
+ const page = await browser.newPage();
37
+ await page.setContent(html, { waitUntil: "load" });
38
+ await page.pdf({ path: outFile, format: "A4", printBackground: true, displayHeaderFooter: true, headerTemplate: "<span></span>", footerTemplate: `<div style="font:8pt Segoe UI;color:#999;width:100%;text-align:center"><span class="pageNumber"></span>/<span class="totalPages"></span></div>`, margin: { top: "14mm", bottom: "16mm", left: "14mm", right: "14mm" } });
39
+ await browser.close();
40
+ }