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
package/src/markup.js ADDED
@@ -0,0 +1,116 @@
1
+ import katex from "katex";
2
+ import { tableHTML } from "./table.js";
3
+
4
+ // Marcação inline usada em qualquer texto do YAML.
5
+ // **negrito** *itálico* ==marca-texto== ^^cor de ênfase^^
6
+ // ~~riscado~~ t~c~ (índice) m^2^ (expoente) `código` [link](https://...) [outro slide](#id) quebra de linha = \n
7
+ // $fórmula$ (LaTeX no meio do texto) e $$fórmula$$ (em destaque). Dinheiro não vira fórmula: "R$ 10", "$5 e $6".
8
+ // | a | b | (uma linha por linha da tabela, com ou sem |---|) vira tabela de verdade
9
+ // a regra do Pandoc: abre sem espaço depois e fecha sem espaço antes (e sem letra ou número logo depois). "$2$" e
10
+ // "$1 - P(X \geq x)$" são fórmula; "R$ 10", "$5 ou $6" e "de $5 a $10" continuam dinheiro (o "fecho" teria espaço antes)
11
+ export const INLINE_MATH = /\$\$([^$]+?)\$\$|(?<![\w$\\])\$(?!\s)([^$\n]+?)(?<!\s)\$(?![\w])/g;
12
+
13
+ export function esc(s) {
14
+ return String(s ?? "")
15
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
16
+ .replace(/"/g, "&quot;");
17
+ }
18
+
19
+ // tabela em markdown no meio do texto (uma linha "| a | b |" por linha, com ou sem a linha "|---|---|" depois do
20
+ // cabeçalho): vira tabela de verdade (src/table.js), não texto com barras
21
+ const PIPE_TABLE = /(^|\n)((?:[ \t]*\|[^\n]*\|[ \t]*(?:\n|$)){2,})/g;
22
+ const PIPE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/;
23
+ function pipeTable(block) {
24
+ const lines = block.trim().split("\n").map((l) => l.trim());
25
+ const cells = (l) => l.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
26
+ const sep = lines.findIndex((l) => PIPE_SEP.test(l));
27
+ const head = sep === 1 ? cells(lines[0]) : null;
28
+ const rows = lines.filter((l, i) => !PIPE_SEP.test(l) && !(head && i === 0)).map(cells);
29
+ return tableHTML({ head, rows }, { cls: "md-table", style: "font-size:.85em;" });
30
+ }
31
+
32
+ // Outros jeitos que a IA usa e que querem dizer o mesmo:
33
+ // - \( … \) e \[ … \] (os delimitadores do LaTeX) → $…$ e $$…$$
34
+ // - a tabela inteira numa linha só ("| Ano | Q | |---|---| | 1984 | 1796 |": o YAML juntou as linhas) → uma por linha
35
+ const LATEX_DELIMS = (t) => t.replace(/\\\[([\s\S]+?)\\\]/g, (_, x) => `$$${x.trim()}$$`).replace(/\\\(([\s\S]+?)\\\)/g, (_, x) => `$${x.trim()}$`);
36
+ function oneLineTables(t) {
37
+ return t.split("\n").map((line) => {
38
+ const a = line.indexOf("|"), b = line.lastIndexOf("|");
39
+ if (a < 0 || b <= a || !/\|\s*:?-{3,}/.test(line) || (line.match(/\|/g) || []).length < 6) return line;
40
+ const rows = line.slice(a, b + 1).split(/\|\s+\|/).map((r) => `| ${r.replace(/^\|\s*/, "").replace(/\s*\|$/, "")} |`);
41
+ if (rows.length < 3) return line;
42
+ return `${line.slice(0, a).trimEnd()}${a ? "\n" : ""}${rows.join("\n")}${line.slice(b + 1).trim() ? `\n${line.slice(b + 1).trim()}` : ""}`;
43
+ }).join("\n");
44
+ }
45
+
46
+ export function md(s) {
47
+ if (s == null) return "";
48
+ const tables = [];
49
+ const maths = [];
50
+ const src = oneLineTables(LATEX_DELIMS(String(s).trim())).replace(PIPE_TABLE, (m, lead, block) => {
51
+ try { tables.push(pipeTable(block)); } catch { return m; }
52
+ return `${lead}\u0002${tables.length - 1}\u0002${block.endsWith("\n") ? "\n" : ""}`;
53
+ });
54
+ const raw = src.replace(INLINE_MATH, (_, block, inline) => {
55
+ // \\frac (barra dobrada, de YAML em bloco escrito como se fosse entre aspas): em LaTeX \\ é quebra de linha e a
56
+ // fórmula saía partida ("TR =" e "frac1P"); \\ seguido de letra é sempre o comando (a quebra vem com espaço)
57
+ const tex = (block ?? inline).trim().replace(/\\\\(?=[A-Za-z])/g, "\\");
58
+ // fórmula que não compila (chave a mais, comando errado): aparece o texto dela, marcado, para corrigir; nunca "undefined"
59
+ try { maths.push(katex.renderToString(tex, { displayMode: block != null, throwOnError: true, strict: "ignore", trust: false, maxExpand: 1000, maxSize: 20 })); }
60
+ catch (e) { maths.push(`<code class="f-mono tex-error" title="${esc(String(e.message || e).slice(0, 200))}">${esc(tex)}</code>`); }
61
+ return `\u0001${maths.length - 1}\u0001`;
62
+ });
63
+ let h = esc(raw);
64
+ const codes = [];
65
+ h = h.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return `\u0000${codes.length - 1}\u0000`; });
66
+ h = h
67
+ .replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
68
+ // [texto](#id): link para outro slide da apresentação (navegação por caminhos; ver goto no build)
69
+ .replace(/\[([^\]]+)\]\(#([\w-]+)\)/g, '<a class="goto-link" href="#s-$2" data-goto="$2">$1</a>')
70
+ .replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>")
71
+ .replace(/(^|[^*])\*([^*\s][^*]*?)\*/g, "$1<i>$2</i>")
72
+ .replace(/==([^=]+)==/g, "<mark>$1</mark>")
73
+ .replace(/\^\^([^^]+)\^\^/g, '<span class="em">$1</span>')
74
+ .replace(/~~([^~]+)~~/g, "<s>$1</s>")
75
+ // índice e expoente como no Pandoc (t~c~, m^2^): sem espaço dentro ("cerca de ~5 km e ~10 m" é texto)
76
+ .replace(/(?<!~)~([^~\s<>]{1,20})~(?!~)/g, "<sub>$1</sub>")
77
+ .replace(/(?<!\^)\^([^\^\s<>]{1,20})\^(?!\^)/g, "<sup>$1</sup>")
78
+ .replace(/\n/g, "<br>");
79
+ h = h.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code class="f-mono">${codes[+i]}</code>`);
80
+ h = h.replace(/\u0001(\d+)\u0001/g, (_, i) => maths[+i]);
81
+ h = h.replace(/(?:<br>)?\u0002(\d+)\u0002(?:<br>)?/g, (_, i) => tables[+i]);
82
+ return h;
83
+ }
84
+
85
+ // texto puro (para notas do PowerPoint e contagem de palavras)
86
+ export function plain(s) {
87
+ return String(s ?? "")
88
+ .replace(/\*\*|==|\^\^|~~|`/g, "")
89
+ .replace(/~([^~\s]{1,20})~|\^([^^\s]{1,20})\^/g, "$1$2")
90
+ .replace(/(^|[^*])\*([^*]+)\*/g, "$1$2")
91
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
92
+ }
93
+
94
+ // Markdown simples para notas/roteiro: parágrafos, "- " listas, "> " falas, "## " títulos
95
+ export function notesHTML(s) {
96
+ if (!s) return "";
97
+ const lines = String(s).replace(/\r/g, "").split("\n");
98
+ let out = "", list = false;
99
+ const close = () => { if (list) { out += "</ul>"; list = false; } };
100
+ for (const raw of lines) {
101
+ const l = raw.trimEnd();
102
+ if (!l.trim()) { close(); continue; }
103
+ if (/^\s*- /.test(l)) { if (!list) { out += "<ul>"; list = true; } out += `<li>${md(l.replace(/^\s*- /, ""))}</li>`; continue; }
104
+ close();
105
+ if (/^## /.test(l)) out += `<h4>${md(l.slice(3))}</h4>`;
106
+ else if (/^> /.test(l)) out += `<p class="say">${md(l.slice(2))}</p>`;
107
+ else if (/^[A-ZÁÉÍÓÚÂÊÔÃÕÇ]{3,}[^:]{0,28}:/.test(l)) out += `<p><span class="tag">${esc(l.match(/^([^:]+):/)[1])}</span>${md(l.replace(/^[^:]+:/, ""))}</p>`;
108
+ else out += `<p>${md(l)}</p>`;
109
+ }
110
+ close();
111
+ return out;
112
+ }
113
+
114
+ export function notesPlain(s) {
115
+ return plain(String(s ?? "").replace(/^> /gm, "“").replace(/^## /gm, ""));
116
+ }
package/src/master.js ADDED
@@ -0,0 +1,214 @@
1
+ // Mestre do deck (como o slide mestre do PowerPoint): o que se desenha em todo slide (logos, faixas, linha do título,
2
+ // número da página) e onde o conteúdo e o título ficam. Com o tema (cores e fontes), é o "estilo" que um professor
3
+ // usa para deixar os slides no padrão da universidade.
4
+ //
5
+ // master:
6
+ // elements: [ … ] # elementos do canvas (x, y, w, h) em todo slide de conteúdo
7
+ // cover: [ … ] # capa, seção e encerramento (sem isso, usam `elements`)
8
+ // area: { top, left, right, bottom } # onde o conteúdo fica (px), fora da moldura
9
+ // coverArea: { top, left, right, bottom }
10
+ // title: { font, size, color, bold, italic, upper, align } # a cara do título
11
+ // slide: master: false # este slide sem a moldura
12
+ // No texto do mestre, o trecho com field: slidenum mostra o número do slide.
13
+ import { esc } from "./markup.js";
14
+
15
+ const COVER = new Set(["cover", "section", "end"]);
16
+ export const isCoverLayout = (layout) => COVER.has(layout);
17
+
18
+ export function masterApplies(spec, s, layout) {
19
+ const m = spec?.master;
20
+ if (!m || s?.master === false) return false;
21
+ if (layout === "canvas" && s?.master !== true) return false; // slide livre (e o original importado) já tem a sua moldura
22
+ return !!(m.elements?.length || m.cover?.length || m.area || m.title);
23
+ }
24
+
25
+ // elementos do mestre para o slide i (número da página preenchido)
26
+ export function masterElements(spec, layout, i) {
27
+ const m = spec.master;
28
+ const list = isCoverLayout(layout) && Array.isArray(m.cover) ? m.cover : m.elements || [];
29
+ const fill = (e) => {
30
+ if (!e?.textbox) return e;
31
+ const c = structuredClone(e);
32
+ for (const p of c.textbox.paragraphs || []) for (const r of p.runs || []) if (r.field === "slidenum") r.t = String(i + 1);
33
+ return c;
34
+ };
35
+ return list.map(fill);
36
+ }
37
+
38
+ const num = (v, d) => (Number.isFinite(Number(v)) ? Number(v) : d);
39
+ const px = (v) => `${Math.round(num(v, 0) * 10) / 10}px`;
40
+ const areaCSS = (a) => (a ? `top:${px(a.top)};left:${px(a.left)};right:${px(a.right)};bottom:${px(a.bottom)};` : "");
41
+ const fontStack = (f) => (f ? `'${String(f).replace(/'/g, "")}', Calibri, Carlito, 'Segoe UI', Arial, sans-serif` : "");
42
+
43
+ // Faixa do título, como no PowerPoint: do topo da área até a linha (fio) do título que a moldura desenha. O título do
44
+ // slide fica nela (encolhe para caber) e o conteúdo começa abaixo da linha: nada atravessa o fio. Sem fio na moldura,
45
+ // não há faixa (o título segue no fluxo do layout, como antes). Calculada da moldura: vale para deck já gerado.
46
+ export function titleBand(spec) {
47
+ const m = spec?.master;
48
+ if (!m?.area || !Array.isArray(m.elements)) return null;
49
+ const H = spec.aspect === "4:3" ? 1440 : 1080;
50
+ const size = m.title?.size || 64;
51
+ const rule = m.elements
52
+ .filter((e) => e.drawing && Number(e.h) <= 8 && Number(e.w) >= 700 && e.y > m.area.top + size * 0.5 && e.y < H * 0.35)
53
+ .sort((a, b) => a.y - b.y)[0];
54
+ if (!rule) return null;
55
+ const top = m.area.top, bottom = Math.round(rule.y - 6);
56
+ const contentTop = Math.round(Math.max(rule.y + 24, top + size * 1.12 + (m.title?.gap ?? 40)));
57
+ return { top, bottom, contentTop };
58
+ }
59
+
60
+ export function masterCSS(spec) {
61
+ const m = spec?.master;
62
+ if (!m) return "";
63
+ let css = "";
64
+ if (m.area) css += `.slide.has-master:not(.master-cover) .safe{${areaCSS(m.area)}}\n`;
65
+ const band = titleBand(spec);
66
+ if (band) {
67
+ css += `.slide.has-master.master-band:not(.master-cover) .safe{top:${band.contentTop}px}\n`;
68
+ const a = m.area, h = band.bottom - band.top - 10;
69
+ css += `.slide.has-master.master-band>.master-title{position:absolute;z-index:2;left:${a.left}px;right:${a.right}px;top:${band.top}px;height:${band.bottom - band.top}px;padding-bottom:10px;box-sizing:border-box;display:flex;align-items:flex-end}\n`;
70
+ // sem o espaçamento do cabeçalho do layout (o padding e o gap dele roubavam altura da faixa e o título encolhia à toa)
71
+ css += `.slide.has-master.master-band .master-title>.hd{margin:0!important;width:100%;max-height:100%;gap:0!important;padding:0!important}.slide.has-master.master-band .master-title>.hd>.kicker{margin-bottom:8px}.slide.has-master.master-band .master-title .ttl{max-height:${h}px;overflow:hidden;box-sizing:border-box;padding-bottom:.1em}\n`;
72
+ }
73
+ if (m.coverArea) css += `.slide.has-master.master-cover .safe{${areaCSS(m.coverArea)}}\n`;
74
+ const t = m.title;
75
+ if (t) {
76
+ const st = [
77
+ t.font && `font-family:${fontStack(t.font)}!important`,
78
+ t.size && `font-size:${px(t.size)}!important`,
79
+ t.color && `color:${/^#/.test(t.color) ? t.color : `#${t.color}`}!important`,
80
+ `font-weight:${t.bold ? 700 : 400}!important`,
81
+ `font-style:${t.italic ? "italic" : "normal"}!important`,
82
+ `text-transform:${t.upper ? "uppercase" : "none"}!important`,
83
+ "letter-spacing:0!important", "line-height:1.12!important", "font-stretch:100%!important",
84
+ t.align && `text-align:${t.align}`,
85
+ ].filter(Boolean).join(";");
86
+ css += `.slide.has-master:not(.master-cover) .hd .ttl{${st}}\n`;
87
+ if (t.gap != null) css += `.slide.has-master:not(.master-cover) .hd{margin-bottom:${px(t.gap)}}\n`;
88
+ css += `.slide.has-master:not(.master-cover) .hd .kicker{display:none}\n`;
89
+ // a linha/fita do título do tema some: a moldura do estilo tem a sua
90
+ css += `.slide.has-master .hd{border:0!important;box-shadow:none!important;background:none!important}.slide.has-master .hd::before,.slide.has-master .hd::after,.slide.has-master .orn{display:none!important}\n`;
91
+ }
92
+ if (m.text?.font) css += `.slide.has-master .safe .t.f-body{font-family:${fontStack(m.text.font)}}\n`;
93
+ return css;
94
+ }
95
+
96
+ // ------------------------------------------------------------------------------------------------ estilo do original
97
+ // A partir de um deck importado (src/import): o layout mais usado vira a moldura de conteúdo; o do 1º slide, a capa.
98
+ // Título e corpo: posição, fonte, tamanho e cor dos placeholders do original. Tema: cores e fontes do arquivo.
99
+ const plainText = (e) => (e.textbox?.paragraphs || []).map((p) => (p.runs || []).map((r) => r.t || "").join("")).join("\n").trim();
100
+
101
+ // Moldura de slides importados = o que se repete na mesma posição (do mestre ou copiado slide a slide, como muita gente
102
+ // faz). "É o mesmo elemento": imagem/texto iguais, ou desenho com o mesmo tipo de traço e a mesma cor, na mesma região
103
+ // (arredondada: a linha do título muda alguns pixels de um slide para outro). 20% dos slides: aulas costumam misturar
104
+ // slides de fontes diferentes; o estilo é o do primeiro bloco coerente.
105
+ export function repeatedFrame(pool) {
106
+ // outro tipo (texto, tabela…): o conteúdo, sem a posição (antes "?" e dois conteúdos no mesmo lugar contavam como um)
107
+ const restOf = (e) => { const { x, y, w, h, step, exit, ...r } = e; return JSON.stringify(r).slice(0, 300); };
108
+ const drawKey = (d) => `${(String(d).match(/<path d="[A-Za-z]/g) || []).join("")}|${(String(d).match(/(fill|stroke)="(#[0-9A-Fa-f]{6}|rgba\([^)]*\))"/g) || []).join(",")}`;
109
+ const keyOf = (e) => `${e.image ? `I:${e.image}` : e.drawing ? `S:${drawKey(e.drawing)}` : e.textbox ? `T:${plainText(e)}` : `X:${restOf(e)}`}@${Math.round(e.x / 16)},${Math.round(e.y / 16)},${Math.round(e.w / 48)},${Math.round(e.h / 16)}`;
110
+ const freq = new Map();
111
+ for (const s of pool) for (const k of new Set((s.elements || []).filter((e) => e.deco || (!e.ph && !e.textbox) || (!e.ph && e.textbox && plainText(e).length < 60)).map(keyOf))) freq.set(k, (freq.get(k) || 0) + 1);
112
+ const repeated = new Set([...freq].filter(([, n]) => n >= Math.max(2, pool.length * 0.2)).map(([k]) => k));
113
+ // número da página, data e rodapé do original também são moldura (o número antigo nem bate com o deck novo)
114
+ return { keyOf, repeated, isFrame: (e) => !!e && (e.deco || ["sldNum", "dt", "ftr"].includes(e.ph) || (!e.ph && repeated.has(keyOf(e)))) };
115
+ }
116
+
117
+ // Apresentação recriada (tema novo): a moldura antiga (faixas, logos, número) dos slides originais que ficaram nela,
118
+ // achada pela repetição entre eles; null quando o deck não é recriado. Guardada por lista de slides.
119
+ const FRAMES = new WeakMap();
120
+ export function recreatedFrame(spec) {
121
+ if (!spec?.recreatedFrom || !Array.isArray(spec.slides)) return null;
122
+ if (!FRAMES.has(spec.slides)) {
123
+ const orig = spec.slides.filter((s) => s?.layout === "canvas" && s.original && Array.isArray(s.elements));
124
+ FRAMES.set(spec.slides, orig.length >= 2 ? repeatedFrame(orig).isFrame : (e) => !!e?.deco);
125
+ }
126
+ return FRAMES.get(spec.slides);
127
+ }
128
+
129
+ export function styleFromImport(spec, { name } = {}) {
130
+ const slides = (spec.slides || []).filter((s) => s.layout === "canvas" && Array.isArray(s.elements));
131
+ if (!slides.length) throw new Error("Este deck não veio de uma importação (não há slides do original para copiar o estilo).");
132
+ const H = spec.aspect === "4:3" ? 1440 : 1080;
133
+ const fonts = spec.import?.fonts || {};
134
+ const realFont = (f) => { const t = String(f || "").replace(/\s*\((Body|Headings|Corpo|Títulos|Titulos)\)\s*$/i, "").trim(); return /\((Body|Corpo)\)/i.test(f || "") ? fonts.minor || t : /\((Headings|Títulos|Titulos)\)/i.test(f || "") ? fonts.major || t : t; };
135
+ const clean = (e) => { const c = structuredClone(e); delete c.deco; delete c.ph; if (c.textbox) for (const p of c.textbox.paragraphs || []) for (const r of p.runs || []) if (r.font) r.font = realFont(r.font); return c; };
136
+ const pool = slides.length > 1 ? slides.slice(1) : slides; // todos menos a capa (há slides sem placeholder, só com desenho)
137
+ const { keyOf, repeated } = repeatedFrame(pool);
138
+ const ref = pool.find((s) => s.elements.some((e) => e.ph === "title") && s.elements.some((e) => e.ph === "body") && s.elements.some((e) => repeated.has(keyOf(e)))) || pool[0];
139
+ const frame = ref.elements.filter((e) => repeated.has(keyOf(e)) || (e.deco && !e.ph)).map(clean);
140
+ const pageNum = ref.elements.find((e) => e.ph === "sldNum" && e.textbox) || ref.elements.find((e) => e.textbox && e.textbox.paragraphs.some((p) => p.runs.some((r) => r.field === "slidenum")));
141
+ if (pageNum && !frame.some((e) => e.textbox?.paragraphs.some((p) => p.runs.some((r) => r.field === "slidenum")))) {
142
+ const c = clean(pageNum);
143
+ for (const p of c.textbox.paragraphs) for (const r of p.runs) r.field = "slidenum";
144
+ frame.push(c);
145
+ }
146
+ // capa: o que não é texto do título/subtítulo (faixa, logos) + o texto institucional que se repete no fim
147
+ const cover = slides[0], last = slides[slides.length - 1];
148
+ const lastTexts = new Set(last.elements.filter((e) => e.textbox).map(plainText));
149
+ const coverFrame = cover.elements.filter((e) => !["title", "ctrTitle", "subTitle"].includes(e.ph) && (!e.textbox || lastTexts.has(plainText(e)) || e.deco)).map(clean);
150
+ const run0 = (e) => e?.textbox?.paragraphs?.flatMap((p) => p.runs || []).find((r) => r.t && r.t.trim()) || {};
151
+ // título e corpo: os placeholders; sem eles (caixas de texto comuns), a maior fonte no terço de cima e a maior
152
+ // caixa abaixo dela
153
+ const loose = ref.elements.filter((e) => e.textbox && !repeated.has(keyOf(e)) && plainText(e));
154
+ const title = ref.elements.find((e) => e.ph === "title") || loose.filter((e) => e.y < H * 0.33).sort((a, b) => (run0(b).size || 0) - (run0(a).size || 0))[0];
155
+ const body = ref.elements.find((e) => e.ph === "body") || loose.filter((e) => e !== title && (!title || e.y >= title.y + title.h * 0.5)).sort((a, b) => b.w * b.h - a.w * a.h)[0];
156
+ const tr = run0(title), br = run0(body);
157
+ const tSize = tr.size || 64;
158
+ const pad = (e, k) => (e?.textbox?.pad ? e.textbox.pad[k] : 0);
159
+ // topo visual do título (a caixa do original pode estar ancorada no meio ou embaixo)
160
+ const titleTop = title ? (() => {
161
+ const lines = Math.max(1, (title.textbox.paragraphs || []).length);
162
+ const textH = tSize * 1.2 * lines, a = title.textbox.anchor, inner = title.h - pad(title, 1) - pad(title, 3);
163
+ return title.y + pad(title, 1) + (a === "bottom" ? Math.max(0, inner - textH) : a === "middle" ? Math.max(0, (inner - textH) / 2) : 0);
164
+ })() : 92;
165
+ const bottoms = frame.filter((e) => e.y > H * 0.7).map((e) => e.y);
166
+ const footerTop = bottoms.length ? Math.min(...bottoms) : H - 72;
167
+ const area = {
168
+ top: Math.max(16, Math.round(titleTop)),
169
+ left: Math.max(24, Math.round(Math.min(title ? title.x + pad(title, 0) : 120, body ? body.x + pad(body, 0) : 1e9))),
170
+ right: Math.max(24, Math.round(1920 - Math.max(title ? title.x + title.w - pad(title, 2) : 1800, body ? body.x + body.w - pad(body, 2) : 0))),
171
+ bottom: Math.max(24, Math.round(H - footerTop + 16)),
172
+ };
173
+ // vão entre o título e o conteúdo: até onde o corpo começa no original (a linha do título fica no meio)
174
+ const bodyTop = body ? body.y + pad(body, 1) : area.top + tSize * 1.2 + 56;
175
+ const gap = Math.max(16, Math.round(bodyTop - (area.top + tSize * 1.12)));
176
+ const coverTexts = cover.elements.filter((e) => ["title", "ctrTitle", "subTitle"].includes(e.ph));
177
+ const coverArea = coverTexts.length ? {
178
+ top: Math.max(16, Math.round(Math.min(...coverTexts.map((e) => e.y)))),
179
+ left: Math.max(24, Math.round(Math.min(...coverTexts.map((e) => e.x)))),
180
+ right: Math.max(24, Math.round(1920 - Math.max(...coverTexts.map((e) => e.x + e.w)))),
181
+ bottom: Math.max(24, Math.round(H - Math.max(...coverTexts.map((e) => e.y + e.h)))),
182
+ } : null;
183
+ const c = spec.import?.colors || {};
184
+ const hex = (v, d) => String(v || d).replace("#", "").toUpperCase();
185
+ const tFont = realFont(tr.font) || fonts.major || "Calibri", bFont = realFont(br.font) || fonts.minor || "Calibri";
186
+ const theme = {
187
+ extends: "manual",
188
+ colors: { paper: hex(c.lt1, "FFFFFF"), ink: hex(c.dk1, "000000"), accent: hex(c.accent1, "4472C4"), alert: hex(c.accent2, "ED7D31"), c1: hex(c.accent1, "4472C4"), c2: hex(c.accent2, "ED7D31"), c3: hex(c.accent3, "A5A5A5"), c4: hex(c.accent4, "FFC000"), c5: hex(c.accent5, "5B9BD5"), line: hex(c.lt2, "D9D9D9") },
189
+ faces: {
190
+ display: { css: `font-family: ${fontStack(tFont)}; font-weight: ${tr.b ? 700 : 400}; line-height: 1.1;`, pptx: { face: tFont, bold: !!tr.b } },
191
+ heading: { css: `font-family: ${fontStack(tFont)}; font-weight: 700; line-height: 1.15;`, pptx: { face: tFont, bold: true } },
192
+ body: { css: `font-family: ${fontStack(bFont)}; font-weight: 400; line-height: 1.35;`, pptx: { face: bFont }, pptxBold: { face: bFont, bold: true } },
193
+ label: { css: `font-family: ${fontStack(bFont)}; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; line-height: 1.2;`, pptx: { face: bFont, bold: true } },
194
+ },
195
+ };
196
+ const master = {
197
+ elements: frame,
198
+ cover: coverFrame,
199
+ area,
200
+ ...(coverArea ? { coverArea } : {}),
201
+ title: { font: tFont, size: Math.round(Math.min(tSize, 80)), color: tr.color || `#${theme.colors.ink}`, bold: !!tr.b, align: title?.textbox?.paragraphs?.[0]?.align || "left", gap },
202
+ text: { font: bFont },
203
+ };
204
+ return { name: name || spec.import?.from?.replace(/\.[a-z]+$/i, "") || spec.title || "Estilo importado", theme, master, from: spec.import?.from || null, body: { size: br.size || 40, color: br.color || null } };
205
+ }
206
+
207
+ // imagens que o mestre usa (para copiar junto com o estilo)
208
+ export function masterImages(master) {
209
+ const out = new Set();
210
+ const walk = (list) => (list || []).forEach((e) => { if (e?.image) out.add(e.image); if (e?.drawing) for (const m of String(e.drawing).matchAll(/href="media:([^"]+)"/g)) out.add(m[1]); });
211
+ walk(master?.elements); walk(master?.cover);
212
+ return [...out];
213
+ }
214
+ void esc;