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,106 @@
1
+ // Figura de pontos: ícones desenhados com luzinhas (estilo show de drones), com morph de uma
2
+ // forma para outra. Uso: { points: fone } ou { points: { de: fone, para: mic, legenda, legendaPara } }.
3
+ // Só no HTML a troca é animada (clique na figura); PPTX/PDF usam a forma inicial.
4
+ const W = 20, H = 20;
5
+
6
+ const distSeg = (x, y, x0, y0, x1, y1) => {
7
+ const dx = x1 - x0, dy = y1 - y0, L2 = dx * dx + dy * dy || 1;
8
+ const t = Math.max(0, Math.min(1, ((x - x0) * dx + (y - y0) * dy) / L2));
9
+ return Math.hypot(x - (x0 + t * dx), y - (y0 + t * dy));
10
+ };
11
+ const P = {
12
+ disc: (cx, cy, r) => (x, y) => Math.hypot(x - cx, y - cy) <= r,
13
+ ring: (cx, cy, r, th) => (x, y) => Math.abs(Math.hypot(x - cx, y - cy) - r) <= th / 2,
14
+ // arco: ângulos em graus, 0 = direita, anti-horário positivo (como na tela, y para baixo: use a0>a1 para o topo)
15
+ arc: (cx, cy, r, th, a0, a1) => (x, y) => {
16
+ if (Math.abs(Math.hypot(x - cx, y - cy) - r) > th / 2) return false;
17
+ let a = (Math.atan2(y - cy, x - cx) * 180) / Math.PI;
18
+ if (a < 0) a += 360;
19
+ const lo = Math.min(a0, a1), hi = Math.max(a0, a1);
20
+ return a >= lo && a <= hi;
21
+ },
22
+ rect: (x0, y0, w, h) => (x, y) => x >= x0 && x < x0 + w && y >= y0 && y < y0 + h,
23
+ line: (x0, y0, x1, y1, th) => (x, y) => distSeg(x, y, x0, y0, x1, y1) <= th / 2,
24
+ // moldura vazada (só a borda, como os óculos da foto: o vazio de dentro aparece)
25
+ frame: (x0, y0, w, h, th) => (x, y) => {
26
+ const ix = x >= x0 + th / 2 && x < x0 + w - th / 2, iy = y >= y0 + th / 2 && y < y0 + h - th / 2;
27
+ const ox = x >= x0 - th / 2 && x < x0 + w + th / 2, oy = y >= y0 - th / 2 && y < y0 + h + th / 2;
28
+ return ox && oy && !(ix && iy);
29
+ },
30
+ tri: (x0, y0, x1, y1, x2, y2) => (x, y) => {
31
+ const s = (ax, ay, bx, by, cx, cy) => (cx - bx) * (ay - by) - (ax - bx) * (cy - by);
32
+ const d1 = s(x, y, x0, y0, x1, y1), d2 = s(x, y, x1, y1, x2, y2), d3 = s(x, y, x2, y2, x0, y0);
33
+ return !((d1 < 0 || d2 < 0 || d3 < 0) && (d1 > 0 || d2 > 0 || d3 > 0));
34
+ },
35
+ };
36
+ const any = (...fs) => (x, y) => fs.some((f) => f(x, y));
37
+
38
+ // Biblioteca de formas em grade 20×20: contornos vazados (o preto de dentro aparece),
39
+ // nunca massa sólida. Espessura ~1 para o grão ficar fino.
40
+ const SHAPES = {
41
+ // fone de ouvido: arco no topo + duas conchas vazadas
42
+ fone: any(P.arc(10, 10.5, 7, 1.4, 180, 360), P.frame(1.6, 9.5, 3, 6.5, 1.2), P.frame(15.4, 9.5, 3, 6.5, 1.2)),
43
+ // microfone: anel + haste + base
44
+ mic: any(P.ring(10, 5.6, 2.9, 1.4), P.line(10, 8.8, 10, 15, 1.3), P.arc(10, 15.5, 3.4, 1.2, 15, 165), P.line(6.6, 17.6, 13.4, 17.6, 1.2)),
45
+ // documento: moldura + dobra + linhas de texto
46
+ doc: any(
47
+ P.frame(6, 2, 8, 16, 1.2),
48
+ P.line(14, 2, 14, 6, 1.1), P.line(14, 6, 10, 2, 1.1),
49
+ P.line(7.8, 8.6, 12.2, 8.6, 0.8), P.line(7.8, 10.8, 12.2, 10.8, 0.8), P.line(7.8, 13, 11, 13, 0.8),
50
+ ),
51
+ // planilha: moldura + cabeçalho + grade
52
+ planilha: any(
53
+ P.frame(3.5, 3.5, 13, 13, 1.2),
54
+ P.line(3.5, 6.5, 16.5, 6.5, 1),
55
+ P.line(10, 3.5, 10, 16.5, 0.8), P.line(3.5, 10, 16.5, 10, 0.8), P.line(3.5, 13.2, 16.5, 13.2, 0.8),
56
+ ),
57
+ // busca: lupa (já era contorno)
58
+ busca: any(P.ring(8.4, 8.4, 5, 1.6), P.line(12, 12, 17, 17, 1.8)),
59
+ // conversa: balão vazado + rabinho + uma linha de texto
60
+ chat: any(
61
+ P.frame(2.5, 4, 15, 9, 1.2),
62
+ P.line(6, 13, 9, 13, 1.1), P.line(9, 13, 6.5, 17, 1.1), P.line(6.5, 17, 6, 13, 1.1),
63
+ P.line(4.8, 7.5, 15.2, 7.5, 0.8), P.line(4.8, 10, 12.5, 10, 0.8),
64
+ ),
65
+ };
66
+ export const POINT_SHAPES = Object.keys(SHAPES);
67
+
68
+ function sample(shape) {
69
+ const f = SHAPES[shape];
70
+ if (!f) throw new Error(`points: forma desconhecida "${shape}" (vale: ${POINT_SHAPES.join(", ")})`);
71
+ // passo quarto de ponto: areia fina (milhares de grãos), não bolinhas de macarrão
72
+ const pts = [];
73
+ for (let y = 0; y < H; y += 0.25) for (let x = 0; x < W; x += 0.25) if (f(x + 0.125, y + 0.125)) pts.push({ x: x + 0.125, y: y + 0.125 });
74
+ return pts;
75
+ }
76
+
77
+ // ordena pelo ângulo em torno do centroide: pontos vizinhos viajam juntos no morph
78
+ function order(pts) {
79
+ const cx = pts.reduce((a, p) => a + p.x, 0) / pts.length, cy = pts.reduce((a, p) => a + p.y, 0) / pts.length;
80
+ return [...pts].sort((a, b) => Math.atan2(a.y - cy, a.x - cx) - Math.atan2(b.y - cy, b.x - cx));
81
+ }
82
+
83
+ export function pointsSVG(spec, { id = "pts" } = {}) {
84
+ const single = typeof spec === "string" ? spec : spec.de;
85
+ const target = typeof spec === "string" ? null : spec.para;
86
+ const A = order(sample(single));
87
+ const B = target ? order(sample(target)) : null;
88
+ // equaliza: a menor repete os próprios pontos até empatar (o morph só translada)
89
+ const N = Math.max(A.length, B ? B.length : 0);
90
+ const pad = (v) => Array.from({ length: N }, (_, i) => v[i % v.length]);
91
+ const a = pad(A), b = B ? pad(B) : null;
92
+ const dots = a.map((p, i) => {
93
+ // grão de areia: miúdo, opacidade variada; um em cada nove é joia (o magenta da foto)
94
+ const op = (0.5 + (((i * 2654435761) % 50) / 100)).toFixed(2);
95
+ const jewel = i % 9 === 8 ? ` class="pts-dot pts-jewel"` : ` class="pts-dot"`;
96
+ return `<circle${jewel} cx="${p.x.toFixed(2)}" cy="${p.y.toFixed(2)}" r="0.18" fill-opacity="${op}"/>`;
97
+ }).join("");
98
+ const model = b ? `<script class="pts-model" type="application/json">${JSON.stringify({ a, b }).replace(/</g, "\\u003c")}</script>` : "";
99
+ const label = typeof spec === "object" && (spec.legenda || spec.legendaPara)
100
+ ? `<div class="pts-legend f-label" data-legenda-a="${escAttr(spec.legenda || "")}" data-legenda-b="${escAttr(spec.legendaPara || spec.legenda || "")}" aria-live="polite">${escAttr(spec.legenda || "")}</div>`
101
+ : "";
102
+ const wrap = b ? ` data-points-morph="${id}" role="button" tabindex="0" aria-label="Transformar a figura (clique)"` : "";
103
+ return `<div class="fig fig-points pts"${wrap}><svg class="pts-svg" viewBox="0 0 ${W} ${H}" role="img" aria-label="Figura de pontos: ${escAttr(String(target ? `${single} virando ${target}` : single))}">${dots}</svg>${model}${label}</div>`;
104
+ }
105
+
106
+ const escAttr = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
@@ -0,0 +1,29 @@
1
+ // QR code como SVG (um único path, nítido em qualquer tamanho e no PowerPoint).
2
+ // { qr: "https://linkedin.com/in/…", size: 360, label: "LinkedIn", color: "#111", bg: "#fff" }
3
+ // O QR sempre sai escuro sobre claro, com margem ("quiet zone"): é o que os celulares leem melhor,
4
+ // inclusive quando o slide tem tom escuro.
5
+ import qrcode from "qrcode-generator";
6
+
7
+ export function qrSVG(data, { ec = "M", ink = "#111", paper = "#fff", margin = 2 } = {}) {
8
+ const text = String(data ?? "").trim();
9
+ if (!text) throw new Error("qr: informe o texto ou link do QR code (ex.: qr: https://linkedin.com/in/voce)");
10
+ const q = qrcode(0, ec);
11
+ q.addData(text, "Byte");
12
+ q.make();
13
+ const n = q.getModuleCount();
14
+ const size = n + margin * 2;
15
+ let d = "";
16
+ for (let r = 0; r < n; r++) {
17
+ for (let c = 0; c < n; c++) {
18
+ if (!q.isDark(r, c)) continue;
19
+ let run = 1; // junta módulos escuros vizinhos da mesma linha num retângulo só (SVG menor)
20
+ while (c + run < n && q.isDark(r, c + run)) run++;
21
+ d += `M${c + margin} ${r + margin}h${run}v1h-${run}z`;
22
+ c += run - 1;
23
+ }
24
+ }
25
+ return `<svg class="qr-svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" role="img" aria-label="QR code: ${escAttr(text)}">`
26
+ + `<rect width="${size}" height="${size}" fill="${paper}"/><path d="${d}" fill="${ink}"/></svg>`;
27
+ }
28
+
29
+ const escAttr = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
@@ -0,0 +1,48 @@
1
+ // Mapa do Brasil por UF em grade (cartograma de ladrilhos): cada estado é um quadrado na posição aproximada dele.
2
+ // Sem geodados de fora (nada pede coisa à internet) e legível em slide: a cor diz o valor, o rótulo diz a UF.
3
+ import { esc } from "../markup.js";
4
+
5
+ // [coluna, linha] de cada UF, do norte ao sul, do oeste ao leste
6
+ export const UF_GRID = {
7
+ RR: [1, 0], AP: [3, 0],
8
+ AM: [1, 1], PA: [2, 1], MA: [3, 1], CE: [4, 1], RN: [5, 1],
9
+ AC: [0, 2], RO: [1, 2], TO: [2, 2], PI: [3, 2], PE: [4, 2], PB: [5, 2],
10
+ MT: [1, 3], GO: [2, 3], BA: [3, 3], SE: [4, 3], AL: [5, 3],
11
+ MS: [1, 4], DF: [2, 4], MG: [3, 4], ES: [4, 4],
12
+ PR: [1, 5], SP: [2, 5], RJ: [3, 5],
13
+ SC: [1, 6],
14
+ RS: [1, 7],
15
+ };
16
+
17
+ const fmt = (v) => (Number.isFinite(v) ? v.toLocaleString("pt-BR", { maximumFractionDigits: 1 }) : "");
18
+
19
+ // o: { ufmap: { SP: 120, RJ: 80, … }, suffix, prefix, highlight: [SP], legend: "texto", showValues }
20
+ export function ufmap(o, warnings) {
21
+ const raw = o.ufmap && typeof o.ufmap === "object" ? o.ufmap : {};
22
+ const vals = {};
23
+ for (const [k, v] of Object.entries(raw)) {
24
+ const uf = String(k).trim().toUpperCase();
25
+ if (!UF_GRID[uf]) { warnings?.push(`ufmap: "${k}" não é uma UF (use a sigla: SP, RJ, MG…)`); continue; }
26
+ const n = typeof v === "number" ? v : parseFloat(String(v).replace(/\./g, "").replace(",", "."));
27
+ if (Number.isFinite(n)) vals[uf] = n;
28
+ }
29
+ const nums = Object.values(vals);
30
+ const min = nums.length ? Math.min(...nums) : 0, max = nums.length ? Math.max(...nums) : 1;
31
+ const hl = new Set([].concat(o.highlight ?? []).map((x) => String(x).toUpperCase()));
32
+ // mapa pequeno (quadro estreito): só as siglas, os valores ficam ilegíveis
33
+ const S = 100, G = 8, showVal = o.showValues ?? !(o.cw && o.cw < 420);
34
+ const cells = Object.entries(UF_GRID).map(([uf, [c, r]]) => {
35
+ const x = c * (S + G), y = r * (S + G), has = uf in vals;
36
+ // escala de 18% a 100% da cor de destaque: o menor valor ainda aparece
37
+ const k = has ? (max === min ? 100 : Math.round(18 + ((vals[uf] - min) / (max - min)) * 82)) : 0;
38
+ const fill = has ? `color-mix(in srgb,var(--em) ${k}%,var(--surface))` : "color-mix(in srgb,var(--fg) 7%,var(--surface))";
39
+ const ink = has && k > 55 ? "var(--bg)" : "var(--fg)";
40
+ const val = has && showVal ? `<text x="${x + S / 2}" y="${y + 74}" text-anchor="middle" font-size="22" style="fill:${ink}" class="uf-v f-body">${esc(`${o.prefix || ""}${fmt(vals[uf])}${o.suffix || ""}`)}</text>` : "";
41
+ return `<g class="uf-cell${has ? "" : " uf-empty"}${hl.has(uf) ? " uf-hl" : ""}" data-uf="${uf}">
42
+ <rect x="${x}" y="${y}" width="${S}" height="${S}" rx="12" style="fill:${fill}${hl.has(uf) ? ";stroke:var(--fg);stroke-width:6" : ""}"/>
43
+ <text x="${x + S / 2}" y="${y + (val ? 44 : 60)}" text-anchor="middle" font-size="30" font-weight="700" style="fill:${ink}" class="uf-n f-heading">${uf}</text>${val}</g>`;
44
+ });
45
+ const W = 6 * (S + G) - G, H = 8 * (S + G) - G;
46
+ const legend = nums.length ? `<div class="uf-legend f-label"><span>${esc(`${o.prefix || ""}${fmt(min)}${o.suffix || ""}`)}</span><i></i><span>${esc(`${o.prefix || ""}${fmt(max)}${o.suffix || ""}`)}</span>${o.legend ? `<b>${esc(o.legend)}</b>` : ""}</div>` : "";
47
+ return `<div class="ufmap"><svg class="ufmap-svg" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Mapa do Brasil por UF">${cells.join("")}</svg>${legend}</div>`;
48
+ }
@@ -0,0 +1,377 @@
1
+ // sagadeck · Fiscal Inteligente e Motor de Auto-Correção
2
+ // Detecta e repara automaticamente:
3
+ // 1. Sobreposição de elementos (bounding box collision)
4
+ // 2. Transgressão de margens seguras (safe area overflow)
5
+ // 3. Estouro de texto e fontes fora dos limites (horizontal/vertical overflow)
6
+ // 4. Elementos fora das coordenadas do slide (0..1920, 0..1080)
7
+ // 5. Excesso de texto anti-sono (move narrativa secundária para notes:)
8
+ import { wordLimit } from "../purpose.js";
9
+ import { normalizeSpec } from "./normalize.js";
10
+
11
+ export function parseIssueText(text) {
12
+ if (!text) return { target: "", detail: "" };
13
+ const parts = text.split("⟂").map((s) => s.trim());
14
+ return {
15
+ elements: parts,
16
+ target: parts[0] || text,
17
+ detail: parts[1] || "",
18
+ };
19
+ }
20
+
21
+ export function optimizeTriggerTitle(s) {
22
+ if (!s.title || typeof s.title !== "string") return null;
23
+ let title = s.title.trim();
24
+ const original = title;
25
+ let modified = false;
26
+ let newKicker = s.kicker;
27
+
28
+ // 1. Separador tipo "Contexto: Frase de Impacto"
29
+ const sepMatch = title.match(/^([^:–—]+)[:–—-]\s*(.+)$/);
30
+ if (sepMatch && !newKicker && sepMatch[1].trim().split(/\s+/).length <= 4) {
31
+ newKicker = sepMatch[1].trim();
32
+ title = sepMatch[2].trim();
33
+ title = title.charAt(0).toUpperCase() + title.slice(1);
34
+ modified = true;
35
+ }
36
+
37
+ // 2. Limpar preâmbulos prolixos comuns de IA (ex: "A implementação de...", "Um estudo sobre...", etc.)
38
+ const preambles = [
39
+ "A implementação de", "O processo de", "Um estudo detalhado sobre", "Um estudo sobre",
40
+ "Uma visão geral sobre", "Como funciona a", "Como fazer para", "Estratégias eficazes para",
41
+ "Estratégias para", "Principais desafios na", "Principais desafios em"
42
+ ];
43
+ for (const p of preambles) {
44
+ const re = new RegExp(`^${p}\\s+`, "i");
45
+ if (re.test(title)) {
46
+ title = title.replace(re, "").trim();
47
+ title = title.charAt(0).toUpperCase() + title.slice(1);
48
+ modified = true;
49
+ break;
50
+ }
51
+ }
52
+
53
+ // (Removidas: cortar o título nas 5 primeiras palavras e pintar uma palavra com ==marca-texto==.
54
+ // Cortavam frases no meio e deixavam todos os títulos com o mesmo destaque. Ênfase é decisão de
55
+ // quem escreve; título longo é tratado pelo ajuste de tamanho (fit).)
56
+
57
+ if (modified) {
58
+ // o título original fica registrado em `auto` (antes), sem poluir as anotações
59
+ s.title = title;
60
+ if (newKicker && !s.kicker) s.kicker = newKicker;
61
+ return `Título longo/spoiler encurtado para título-gatilho ("${title}"); tese completa preservada nas notas.`;
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // Toda mudança da auto-correção fica registrada no próprio slide, em `auto`:
67
+ // auto: [{ campo, antes, motivo, quando }]
68
+ // Assim o Studio mostra (e desfaz) o que foi automático, e a IA sabe que não foi ela nem o usuário.
69
+ export function autofixSlide(slide, spec = {}, issues = []) {
70
+ const res = autofixSlideCore(slide, spec, issues);
71
+ if (!res.modified) return res;
72
+ res.slide.auto = recordAuto(slide, res.slide, res.actions);
73
+ return res;
74
+ }
75
+
76
+ const AUTO_SKIP = new Set(["auto"]);
77
+ export function recordAuto(before, after, actions, por = "auto-correção") {
78
+ const log = Array.isArray(before.auto) ? before.auto.map((e) => ({ ...e })) : [];
79
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
80
+ const motivo = actions.join("; ");
81
+ for (const k of keys) {
82
+ if (AUTO_SKIP.has(k) || k.startsWith("_")) continue;
83
+ if (JSON.stringify(before[k]) === JSON.stringify(after[k])) continue;
84
+ const prev = log.find((e) => e.campo === k);
85
+ if (prev) { prev.motivo = `${prev.motivo}; ${motivo}`; continue; } // mantém o valor original mais antigo
86
+ log.push({ campo: k, antes: before[k] === undefined ? null : before[k], motivo, por, quando: new Date().toISOString().slice(0, 16).replace("T", " ") });
87
+ }
88
+ return log;
89
+ }
90
+
91
+ function autofixSlideCore(slide, spec = {}, issues = []) {
92
+ const s = JSON.parse(JSON.stringify(slide));
93
+ const actions = [];
94
+ const layout = s.layout || "blocks";
95
+
96
+ // 1. Se recebemos lista de problemas do fiscal / DOM
97
+ if (Array.isArray(issues) && issues.length > 0) {
98
+ for (const issue of issues) {
99
+ const kind = issue.kind;
100
+
101
+ // --- Caso: Sobreposição ---
102
+ if (kind === "sobreposicao") {
103
+ if (layout === "canvas" && Array.isArray(s.elements)) {
104
+ // No canvas, calcular caixas e empurrar elemento de baixo
105
+ let adjusted = false;
106
+ for (let i = 0; i < s.elements.length; i++) {
107
+ for (let j = i + 1; j < s.elements.length; j++) {
108
+ const A = s.elements[i];
109
+ const B = s.elements[j];
110
+ const ax = A.x ?? 0, ay = A.y ?? 0, aw = A.w ?? 400, ah = A.h ?? 150;
111
+ const bx = B.x ?? 0, by = B.y ?? 0, bw = B.w ?? 400, bh = B.h ?? 150;
112
+ const ix = Math.min(ax + aw, bx + bw) - Math.max(ax, bx);
113
+ const iy = Math.min(ay + ah, by + bh) - Math.max(ay, by);
114
+ if (ix > 8 && iy > 8) {
115
+ // Sobreposição detectada: empurrar B para baixo de A
116
+ B.y = ay + ah + 24;
117
+ // Garantir que não saia da margem inferior (976px)
118
+ if (B.y + bh > 976) {
119
+ B.y = 976 - bh;
120
+ // Se ainda colidir, alinhar horizontalmente se couber
121
+ if (ax + aw + bw + 24 <= 1800) {
122
+ B.x = ax + aw + 24;
123
+ B.y = ay;
124
+ }
125
+ }
126
+ adjusted = true;
127
+ actions.push(`Reposicionado elemento no canvas para eliminar sobreposição com "${issue.text}"`);
128
+ }
129
+ }
130
+ }
131
+ } else {
132
+ // Layouts padrão: reduzir titleSize ou diminuir altura de blocos
133
+ const curSize = s.titleSize || (layout === "cover" ? 140 : layout === "statement" ? 110 : 92);
134
+ const newSize = Math.max(48, Math.round(curSize * 0.78));
135
+ if (newSize !== curSize) {
136
+ s.titleSize = newSize;
137
+ actions.push(`Reduzido titleSize de ${curSize}px para ${newSize}px para eliminar sobreposição`);
138
+ }
139
+ if (s.fit !== false) s.fit = true;
140
+ }
141
+ }
142
+
143
+ // --- Caso: Passa da Margem Inferior (Overflow vertical da safe area) ---
144
+ if (kind === "passa-da-margem-inferior") {
145
+ const pxOverflow = issue.px || 30;
146
+
147
+ // Se o título estiver grande, reduzi-lo
148
+ const curSize = s.titleSize || (layout === "cover" ? 140 : layout === "section" ? 150 : 92);
149
+ if (curSize > 56) {
150
+ const newSize = Math.max(48, Math.round(curSize * 0.75));
151
+ s.titleSize = newSize;
152
+ actions.push(`Ajustado tamanho do título (${curSize}px -> ${newSize}px) para recolher o conteúdo à margem segura (+${pxOverflow}px)`);
153
+ }
154
+
155
+ // Se layout for cards e estiver em 1 ou 2 colunas com mais de 3 cards, expandir para 3 ou 4 colunas
156
+ if (layout === "cards" && Array.isArray(s.items) && s.items.length >= 3) {
157
+ const curCols = s.cols || (s.items.length >= 4 ? 2 : s.items.length);
158
+ if (curCols < 4 && s.items.length >= 4) {
159
+ s.cols = Math.min(4, s.items.length);
160
+ actions.push(`Expandida grade de cards para ${s.cols} colunas para evitar estouro da margem inferior`);
161
+ }
162
+ }
163
+
164
+ // Se layout for list e tiver muitos itens
165
+ if (layout === "list" && Array.isArray(s.items) && s.items.length > 5) {
166
+ const removed = s.items.slice(5);
167
+ s.items = s.items.slice(0, 5);
168
+ s.notes = (s.notes ? s.notes + "\n\n" : "") + "> Itens adicionais para fala:\n" + removed.map((r) => "- " + (typeof r === "object" ? r.text || r.title : r)).join("\n");
169
+ actions.push(`Movidos ${removed.length} itens excedentes da lista para as notas do apresentador`);
170
+ }
171
+
172
+ // Se houver corpo/texto longo, compactar e mover narrativa para notas
173
+ if (typeof s.body === "string" && s.body.length > 160) {
174
+ const sentences = s.body.split(/(?<=[.?!])\s+/);
175
+ if (sentences.length > 1) {
176
+ const keep = sentences[0];
177
+ const toNotes = sentences.slice(1).join(" ");
178
+ s.body = keep;
179
+ s.notes = (s.notes ? s.notes + "\n\n" : "") + `> Fala complementar: ${toNotes}`;
180
+ actions.push(`Compactado texto do corpo e transferida narrativa detalhada para notas do apresentador`);
181
+ }
182
+ }
183
+ }
184
+
185
+ // --- Caso: Estouro Horizontal / Texto Saindo da Margem ---
186
+ if (kind === "estouro-horizontal") {
187
+ const curSize = s.titleSize || s.size || 92;
188
+ const newSize = Math.max(44, Math.round(curSize * 0.76));
189
+ s.titleSize = newSize;
190
+ s.fit = true;
191
+ actions.push(`Reduzida fonte (${curSize}px -> ${newSize}px) e ativado ajuste automático (fit: true) para evitar estouro horizontal`);
192
+ }
193
+
194
+ // --- Caso: Fora do Slide (coordenadas menores que 0 ou maiores que 1920x1080) ---
195
+ if (kind === "fora-do-slide") {
196
+ if (layout === "canvas" && Array.isArray(s.elements)) {
197
+ s.elements.forEach((el, idx) => {
198
+ const w = el.w || 300;
199
+ const h = el.h || 120;
200
+ const origX = el.x ?? 0;
201
+ const origY = el.y ?? 0;
202
+ // Safe zone: left 120, right 1800, top 92, bottom 976
203
+ const clampedX = Math.max(120, Math.min(origX, 1800 - w));
204
+ const clampedY = Math.max(92, Math.min(origY, 976 - h));
205
+ if (clampedX !== origX || clampedY !== origY) {
206
+ el.x = clampedX;
207
+ el.y = clampedY;
208
+ actions.push(`Elemento #${idx + 1} no canvas reajustado para dentro da área segura ([${clampedX}, ${clampedY}])`);
209
+ }
210
+ });
211
+ }
212
+ }
213
+
214
+ // --- Caso: Baixo Contraste ---
215
+ if (kind === "baixo-contraste") {
216
+ // Alternar tom para garantir legibilidade máxima
217
+ const prevTone = s.tone || "light";
218
+ s.tone = prevTone === "light" ? "dark" : "light";
219
+ delete s.fg;
220
+ delete s.bg;
221
+ actions.push(`Ajustado tom do slide de "${prevTone}" para "${s.tone}" para atender ao contraste mínimo WCAG`);
222
+ }
223
+ }
224
+ }
225
+
226
+ // 2. Verificações heurísticas universais (Anti-sono, Títulos-Gatilho e Geometria Segura)
227
+
228
+ // Otimização de Título-Gatilho (Anti-spoiler / Memória rápida)
229
+ const titleAction = optimizeTriggerTitle(s);
230
+ if (titleAction) actions.push(titleAction);
231
+
232
+ // Regra Anti-sono Inteligente: excesso de palavras no slide
233
+ const words = countSlideWords(s);
234
+ const maxWords = wordLimit(s, spec); // material de consulta/aula: o texto explicativo fica no slide
235
+ if (words > maxWords) {
236
+ let fixedWords = false;
237
+
238
+ // Transbordo do corpo (body) para notas
239
+ if (s.body && typeof s.body === "string" && s.body.split(/\s+/).length > 15) {
240
+ const parts = s.body.split(/(?<=[.?!])\s+/);
241
+ if (parts.length > 1) {
242
+ s.body = parts[0];
243
+ const extra = parts.slice(1).join(" ");
244
+ s.notes = (s.notes ? s.notes + "\n\n" : "") + `> Fala complementar do slide:\n${extra}`;
245
+ fixedWords = true;
246
+ }
247
+ }
248
+
249
+ // Transbordo de cartões (cards com texto longo)
250
+ if (layout === "cards" && Array.isArray(s.items)) {
251
+ s.items.forEach((item, idx) => {
252
+ if (typeof item === "object" && item.text && item.text.split(/\s+/).length > 12) {
253
+ const parts = item.text.split(/(?<=[.?!])\s+/);
254
+ if (parts.length > 1) {
255
+ item.text = parts[0];
256
+ const extra = parts.slice(1).join(" ");
257
+ s.notes = (s.notes ? s.notes + "\n\n" : "") + `> Card #${idx + 1} (${item.title || "Item"}):\n${extra}`;
258
+ fixedWords = true;
259
+ }
260
+ }
261
+ });
262
+ }
263
+
264
+ // Transbordo de bullets excedentes
265
+ if (Array.isArray(s.bullets) && s.bullets.length > 4) {
266
+ const removed = s.bullets.slice(4);
267
+ s.bullets = s.bullets.slice(0, 4);
268
+ s.notes = (s.notes ? s.notes + "\n\n" : "") + `> Tópicos adicionais para o apresentador:\n` + removed.map((b) => `- ${b}`).join("\n");
269
+ fixedWords = true;
270
+ }
271
+
272
+ if (fixedWords) {
273
+ actions.push(`Slide anti-sono: reduzido de ${words} para ${countSlideWords(s)} palavras; excedente transferido para as notas`);
274
+ }
275
+ }
276
+
277
+ // Verificação de Título Excessivamente Longo sem titleSize definido
278
+ if (s.title && typeof s.title === "string" && s.title.length > 55 && !s.titleSize) {
279
+ s.titleSize = layout === "cover" ? 96 : 68;
280
+ actions.push(`Título longo detectado: configurado titleSize=${s.titleSize}px para evitar quebra indesejada`);
281
+ }
282
+
283
+ // Verificação de Colunas de Cards (se 4 ou mais cards, garantir pelo menos 3 colunas)
284
+ if (layout === "cards" && Array.isArray(s.items) && s.items.length >= 4 && (!s.cols || s.cols < 3)) {
285
+ s.cols = Math.min(4, s.items.length);
286
+ actions.push(`Reorganizada distribuição de cards em ${s.cols} colunas`);
287
+ }
288
+
289
+ // Verificação em Canvas livre: garantir que nenhum elemento colida ou fique fora das margens
290
+ if (layout === "canvas" && Array.isArray(s.elements)) {
291
+ // Ordena verticalmente e distribui se colidir
292
+ s.elements.forEach((el) => {
293
+ const w = el.w || 320;
294
+ const h = el.h || 120;
295
+ if (el.x == null) el.x = 120;
296
+ if (el.y == null) el.y = 120;
297
+ el.x = Math.max(120, Math.min(el.x, 1800 - w));
298
+ el.y = Math.max(92, Math.min(el.y, 976 - h));
299
+ });
300
+
301
+ for (let i = 0; i < s.elements.length; i++) {
302
+ for (let j = i + 1; j < s.elements.length; j++) {
303
+ const A = s.elements[i];
304
+ const B = s.elements[j];
305
+ const aw = A.w || 320, ah = A.h || 120;
306
+ const bw = B.w || 320, bh = B.h || 120;
307
+ const ix = Math.min(A.x + aw, B.x + bw) - Math.max(A.x, B.x);
308
+ const iy = Math.min(A.y + ah, B.y + bh) - Math.max(A.y, B.y);
309
+ if (ix > 8 && iy > 8) {
310
+ if (A.y + ah + bh + 24 <= 976) {
311
+ B.y = A.y + ah + 24;
312
+ actions.push(`Resolvida sobreposição entre elementos #${i + 1} e #${j + 1} no canvas`);
313
+ } else if (A.x + aw + bw + 24 <= 1800) {
314
+ B.x = A.x + aw + 24;
315
+ B.y = A.y;
316
+ actions.push(`Alinhado elemento #${j + 1} horizontalmente ao lado do elemento #${i + 1}`);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ return { slide: s, actions, modified: actions.length > 0 };
324
+ }
325
+
326
+ export function autofixDeck(spec, issuesReport = []) {
327
+ const newSpec = normalizeSpec(spec);
328
+ const issuesBySlide = new Map();
329
+
330
+ if (Array.isArray(issuesReport)) {
331
+ for (const item of issuesReport) {
332
+ if (item && item.slide && Array.isArray(item.issues)) {
333
+ issuesBySlide.set(item.slide - 1, item.issues);
334
+ }
335
+ }
336
+ }
337
+
338
+ const allActions = [];
339
+ let totalFixed = 0;
340
+
341
+ newSpec.slides = newSpec.slides.map((rawSlide, index) => {
342
+ const slideIssues = issuesBySlide.get(index) || [];
343
+ const { slide: fixedSlide, actions, modified } = autofixSlide(rawSlide, newSpec, slideIssues);
344
+ if (modified) {
345
+ totalFixed++;
346
+ allActions.push({
347
+ slide: index + 1,
348
+ layout: fixedSlide.layout || "auto",
349
+ actions,
350
+ });
351
+ }
352
+ return fixedSlide;
353
+ });
354
+
355
+ return {
356
+ spec: newSpec,
357
+ modifiedSlidesCount: totalFixed,
358
+ actions: allActions,
359
+ clean: true,
360
+ };
361
+ }
362
+
363
+ function countSlideWords(s) {
364
+ const txt = [];
365
+ const walk = (v, k) => {
366
+ if (k === "notes" || k === "source" || k === "id" || k === "layout" || k === "tone") return;
367
+ if (typeof v === "string") {
368
+ if (!/^(\.|https?:|#?[0-9a-f]{6}$)/i.test(v)) txt.push(v);
369
+ } else if (Array.isArray(v)) {
370
+ v.forEach((x) => walk(x));
371
+ } else if (v && typeof v === "object" && !v.svg && !v.chart && !v.html) {
372
+ for (const [kk, vv] of Object.entries(v)) walk(vv, kk);
373
+ }
374
+ };
375
+ walk(s);
376
+ return txt.join(" ").split(/\s+/).filter((w) => w.length > 1).length;
377
+ }
@@ -0,0 +1,115 @@
1
+ // sagadeck · Normalizador Flexível de Schema (Anti-Alucinação para IAs)
2
+ // Tolerante a variações sintáticas, termos em português e aliases comuns que
3
+ // modelos de IA econômicos (Haiku, Flash, GPT-4o-mini, DeepSeek) possam gerar.
4
+
5
+ const COLOR_MAP = {
6
+ azul: "0066FF",
7
+ verde: "10B981",
8
+ amarelo: "F59E0B",
9
+ vermelho: "EF4444",
10
+ roxo: "8B5CF6",
11
+ rosa: "EC4899",
12
+ cinza: "86868B",
13
+ preto: "1D1D1F",
14
+ branco: "FFFFFF",
15
+ };
16
+
17
+ export function normalizeSpec(rawSpec) {
18
+ if (!rawSpec || typeof rawSpec !== "object") return rawSpec;
19
+ const spec = JSON.parse(JSON.stringify(rawSpec));
20
+
21
+ // 1. Normalização no nível da raiz
22
+ if (spec.tema && !spec.theme) {
23
+ spec.theme = spec.tema;
24
+ delete spec.tema;
25
+ }
26
+ if (spec.titulo && !spec.title) {
27
+ spec.title = spec.titulo;
28
+ delete spec.titulo;
29
+ }
30
+ if (spec.duracao && !spec.duration) {
31
+ spec.duration = spec.duracao;
32
+ delete spec.duracao;
33
+ }
34
+ if (!Array.isArray(spec.slides)) {
35
+ if (Array.isArray(spec.slide)) spec.slides = spec.slide;
36
+ else if (spec.slides && typeof spec.slides === "object") spec.slides = Object.values(spec.slides);
37
+ else spec.slides = [];
38
+ }
39
+
40
+ // 2. Normalização slide a slide
41
+ spec.slides = spec.slides.map((s) => {
42
+ if (!s || typeof s !== "object") return { layout: "statement", text: String(s) };
43
+
44
+ // Termos em português para campos básicos
45
+ if (s.titulo && !s.title) { s.title = s.titulo; delete s.titulo; }
46
+ if (s.subtitulo && !s.subtitle) { s.subtitle = s.subtitulo; delete s.subtitulo; }
47
+ if (s.notas && !s.notes) { s.notes = s.notas; delete s.notas; }
48
+ if (s.fonte && !s.source) { s.source = s.fonte; delete s.fonte; }
49
+ if (s.tempo && !s.time) { s.time = s.tempo; delete s.tempo; }
50
+ if (s.tom && !s.tone) { s.tone = s.tom; delete s.tom; }
51
+
52
+ // Aliases para layout stats (KPIs)
53
+ if (s.kpis || s.metricas) {
54
+ s.stats = s.stats || s.kpis || s.metricas;
55
+ delete s.kpis;
56
+ delete s.metricas;
57
+ }
58
+ if (Array.isArray(s.stats)) {
59
+ s.stats = s.stats.map((st) => {
60
+ if (typeof st !== "object") return { value: String(st) };
61
+ if (st.valor && !st.value) { st.value = st.valor; delete st.valor; }
62
+ if (st.rotulo && !st.label) { st.label = st.rotulo; delete st.rotulo; }
63
+ if (st.tendencia && !st.trend) { st.trend = st.tendencia; delete st.tendencia; }
64
+ if (st.icone && !st.icon) { st.icon = st.icone; delete st.icone; }
65
+ return st;
66
+ });
67
+ }
68
+
69
+ // Aliases para layout steps (Processo)
70
+ if (s.process || s.fluxo || s.passos || s.etapas) {
71
+ s.steps = s.steps || s.process || s.fluxo || s.passos || s.etapas;
72
+ delete s.process;
73
+ delete s.fluxo;
74
+ delete s.passos;
75
+ delete s.etapas;
76
+ }
77
+ if (Array.isArray(s.steps)) {
78
+ s.steps = s.steps.map((st, i) => {
79
+ if (typeof st !== "object") return { stepNum: i + 1, title: String(st) };
80
+ if (st.passo && !st.title) { st.title = st.passo; delete st.passo; }
81
+ if (st.etapa && !st.title) { st.title = st.etapa; delete st.etapa; }
82
+ if (st.descricao && !st.text) { st.text = st.descricao; delete st.descricao; }
83
+ if (st.texto && !st.text) { st.text = st.texto; delete st.texto; }
84
+ if (st.icone && !st.icon) { st.icon = st.icone; delete st.icone; }
85
+ if (st.step && !st.stepNum && typeof st.step === "number") st.stepNum = st.step;
86
+ return st;
87
+ });
88
+ }
89
+
90
+ // Aliases em cards
91
+ if (Array.isArray(s.items)) {
92
+ s.items = s.items.map((it) => {
93
+ if (typeof it !== "object") return it;
94
+ if (it.titulo && !it.title) { it.title = it.titulo; delete it.titulo; }
95
+ if (it.texto && !it.text) { it.text = it.texto; delete it.texto; }
96
+ if (it.icone && !it.icon) { it.icon = it.icone; delete it.icone; }
97
+ if (it.progresso != null && it.progress == null) { it.progress = it.progresso; delete it.progresso; }
98
+ if (it.avaliacao != null && it.rating == null) { it.rating = it.avaliacao; delete it.avaliacao; }
99
+ return it;
100
+ });
101
+ }
102
+
103
+ // Cores comuns em português
104
+ if (s.color && COLOR_MAP[s.color.toLowerCase()]) {
105
+ s.color = COLOR_MAP[s.color.toLowerCase()];
106
+ }
107
+ if (s.bg && COLOR_MAP[s.bg.toLowerCase()]) {
108
+ s.bg = COLOR_MAP[s.bg.toLowerCase()];
109
+ }
110
+
111
+ return s;
112
+ });
113
+
114
+ return spec;
115
+ }