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/lessons.js ADDED
@@ -0,0 +1,171 @@
1
+ // Slides de aula que qualquer matéria usa:
2
+ // solution — exercício resolvido: enunciado e dados à esquerda; cada clique revela o próximo passo da conta;
3
+ // no fim, a resposta em destaque
4
+ // calc — calculadora ao vivo: entradas com controle deslizante, saídas por fórmula (inclusive por faixas:
5
+ // "abaixo de 2300 é laminar"), régua com as faixas e o ponteiro do valor atual
6
+ // A conta usa o mesmo compilador de fórmulas do slide de funções (src/runtime/formula.js): sem eval.
7
+ import katex from "katex";
8
+ import { md, esc } from "./markup.js";
9
+ import "./runtime/formula.js";
10
+ import "./runtime/calc.js";
11
+
12
+ const F = globalThis.SagaFormula;
13
+ const tex = (s, display = true) => katex.renderToString(String(s ?? ""), { displayMode: display, throwOnError: false, strict: "ignore", trust: false, maxExpand: 1000, maxSize: 20 });
14
+ // valor de um dado em LaTeX: número vira vírgula decimal (e 10 elevado, se for muito grande ou muito pequeno)
15
+ const commaTex = (v) => {
16
+ if (typeof v === "number" && Number.isFinite(v)) {
17
+ if (v !== 0 && (Math.abs(v) < 1e-3 || Math.abs(v) >= 1e6)) { const [m, e] = v.toExponential(2).split("e"); return `${String(Number(m)).replace(".", "{,}")}\\times10^{${Number(e)}}`; }
18
+ // 1500 → 1\,500 (espaço fino no milhar) · 1.5 → 1{,}5
19
+ const [int, frac] = String(+v.toFixed(6)).split(".");
20
+ return `${int.replace(/\B(?=(\d{3})+(?!\d))/g, "\\,")}${frac ? `{,}${frac}` : ""}`;
21
+ }
22
+ return String(v ?? "").replace(/(\d),(\d)/g, "$1{,}$2");
23
+ };
24
+ const panel = (i, inner, cls = "") => `<section class="lesson-panel dyn-frame ${cls}${i === 0 ? " active" : ""}" data-lesson-panel="${i}">${inner}</section>`;
25
+
26
+ // ------------------------------------------------------------------------------------------------ solution
27
+ // frase (palavra de 3+ letras, fora sen/log/max…) sem nada de LaTeX: é texto, não fórmula — senão "ver tabela" vira
28
+ // "vertabela" em itálico, sem espaços
29
+ const PROSE = /\p{L}{3,}/u;
30
+ const isProse = (s) => !/\\[a-zA-Z]|[\^_{}]/.test(s) && PROSE.test(String(s).replace(/\b(sen|cos|tan|tg|log|ln|exp|max|min|lim|mod)\b/g, ""));
31
+ const mathOrText = (s) => (isProse(String(s ?? "")) ? `<span class="sol-txt t f-body">${md(String(s))}</span>` : tex(s, false));
32
+ function givenTex(g) {
33
+ if (typeof g === "string") return g;
34
+ if (g.latex) return g.latex;
35
+ const value = typeof g.value === "string" && isProse(g.value) ? `\\text{${g.value.replace(/[{}\\$]/g, "")}}` : commaTex(g.value);
36
+ // % (e # & _ $) são comandos no LaTeX: "60 %" saía como erro vermelho na conta da criança
37
+ const unit = g.unit ? String(g.unit).replace(/[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,") : "";
38
+ return `${g.symbol ?? ""} = ${value}${unit ? `\\ \\mathrm{${unit}}` : ""}`;
39
+ }
40
+ export function solutionHTML(s, ctx, head) {
41
+ const steps = (s.steps || []).map((x) => (typeof x === "string" ? { latex: x } : x || {}));
42
+ const givens = [].concat(s.givens || s.data || []);
43
+ const hasAnswer = !!s.answer;
44
+ const n = 1 + steps.length + (hasAnswer ? 1 : 0); // enunciado · cada passo · resposta
45
+ const left = `<div class="sol-problem">${s.problem ? `<div class="sol-text t f-body">${md(s.problem)}</div>` : ""}
46
+ ${givens.length ? `<div class="sol-givens"><div class="sol-label t f-label">Dados</div>${givens.map((g) => `<div class="sol-given"><div class="sol-tex">${typeof g === "string" ? mathOrText(g) : tex(givenTex(g), false)}</div>${g.label ? `<span class="t f-body">${md(g.label)}</span>` : ""}</div>`).join("")}</div>` : ""}
47
+ ${s.find ? `<div class="sol-find"><span class="sol-label t f-label">Pede-se</span><div class="sol-tex">${mathOrText(s.find)}</div></div>` : ""}</div>`;
48
+ const stepHTML = (st, i, k) => `<article class="sol-step${i === k - 1 ? " now" : ""}"><span class="sol-n t f-label">${String(i + 1).padStart(2, "0")}</span><div class="sol-body">${st.text ? `<div class="sol-why t f-body">${md(st.text)}</div>` : ""}${st.latex ? `<div class="sol-math">${tex(st.latex)}</div>` : ""}${st.note ? `<div class="sol-note t f-body">${md(st.note)}</div>` : ""}</div></article>`;
49
+ const answer = hasAnswer ? (typeof s.answer === "string" ? { latex: s.answer } : s.answer) : null;
50
+ const frames = Array.from({ length: n }, (_, k) => {
51
+ const done = Math.min(k, steps.length);
52
+ const list = steps.slice(0, done).map((st, i) => stepHTML(st, i, done)).join("");
53
+ const final = hasAnswer && k === n - 1 ? `<div class="sol-answer"><span class="sol-label t f-label">${esc(answer.label || "Resposta")}</span>${answer.latex ? `<div class="sol-math">${tex(answer.latex)}</div>` : ""}${answer.text ? `<div class="t f-body">${md(answer.text)}</div>` : ""}</div>` : "";
54
+ const empty = k === 0 ? `<div class="sol-think t f-display">${md(s.prompt || "Por onde você começaria?")}</div>` : "";
55
+ return panel(k, `<div class="sol-steps">${empty}${list}${final}</div>`);
56
+ }).join("");
57
+ return `<div class="L-solution dyn" data-lesson="solution" data-lesson-count="${n}">${head(s)}<div class="sol-row">${left}<div class="dyn-frames">${frames}</div></div></div>`;
58
+ }
59
+
60
+ // ------------------------------------------------------------------------------------------------ calc
61
+ // o modelo da calculadora (o mesmo no motor e na apresentação): entradas, saídas em ordem, faixas e régua
62
+ export function calcModel(s) {
63
+ const inputs = Object.entries(s.inputs || {}).map(([name, v]) => {
64
+ const o = typeof v === "number" ? { value: v } : v || {};
65
+ const value = Number(o.value ?? 1);
66
+ return { name, label: String(o.label || name), unit: o.unit ? String(o.unit) : "", value, min: Number(o.min ?? (value > 0 ? value / 10 : -10)), max: Number(o.max ?? (value > 0 ? value * 10 : 10)), step: Number(o.step ?? ((Number(o.max ?? value * 10) - Number(o.min ?? 0)) / 200 || 0.01)), fixed: !!o.fixed, ...(o.latex ? { latex: String(o.latex) } : {}), ...(o.decimals != null ? { decimals: Number(o.decimals) } : {}) };
67
+ });
68
+ const outputs = [].concat(s.outputs || []).map((o, i) => ({ name: String(o.name || `saida${i + 1}`), label: String(o.label || o.name || `Resultado ${i + 1}`), fn: o.fn ? String(o.fn) : "", of: o.of ? String(o.of) : "", unit: o.unit ? String(o.unit) : "", latex: o.latex ? String(o.latex) : "",
69
+ ...(o.decimals != null ? { decimals: Number(o.decimals) } : {}), ...(o.sci ? { sci: true } : {}),
70
+ cases: [].concat(o.cases || []).map((c) => ({ ...(c.below != null ? { below: Number(c.below) } : {}), ...(c.fn ? { fn: String(c.fn) } : {}), ...(c.text ? { text: String(c.text) } : {}), ...(c.color ? { color: String(c.color) } : {}) })),
71
+ ...(o.scale ? { scale: { min: Number(o.scale.min ?? 0), max: Number(o.scale.max ?? 1), ...(o.scale.log ? { log: true } : {}) } } : {}) }));
72
+ return { inputs, outputs, scenarios: s.scenarios || [], sweep: s.sweep || '', explanation: String(s.explanation || ''), prediction: String(s.prediction || ''), illustrative: !!s.illustrative };
73
+ }
74
+ const C = globalThis.SagaCalc;
75
+ export const calcEvaluate = (m, v) => C.evaluate(m, v);
76
+ const formatNum = (v, o) => C.format(v, o);
77
+ const scalePos = (v, sc) => C.scalePos(v, sc);
78
+ export function calcHTML(s, ctx, head) {
79
+ const m = calcModel(s);
80
+ const res = calcEvaluate(m);
81
+ const errs = Object.entries(res).filter(([, r]) => r.error).map(([k, r]) => `${k}: ${r.error}`);
82
+ if (errs.length) ctx?.warnings?.push(...errs.map((e) => `calculadora: ${e}`));
83
+ const input = (i) => i.fixed
84
+ ? `<div class="calc-in fixed"><span class="calc-l t f-label">${esc(i.label)}</span><b class="calc-const">${i.latex ? tex(i.latex, false) : `${esc(formatNum(i.value, i))}${i.unit ? ` <small>${esc(i.unit)}</small>` : ""}`}</b></div>`
85
+ : `<label class="calc-in"><span class="calc-l t f-label">${esc(i.label)}${i.latex ? ` <span class="calc-sym">${tex(i.latex, false)}</span>` : ""}</span><input type="range" data-calc-in="${esc(i.name)}"${s.inputs?.[i.name]?.decision ? ` data-decision-input="${esc(s.inputs[i.name].decision)}"` : ""} min="${i.min}" max="${i.max}" step="${i.step}" value="${i.value}" aria-label="${esc(i.label)}"><output class="calc-v" data-calc-show="${esc(i.name)}">${esc(formatNum(i.value, i))}</output><small class="calc-u">${esc(i.unit)}</small></label>`;
86
+ const output = (o) => {
87
+ const r = res[o.name];
88
+ const scale = o.scale ? (() => {
89
+ const zones = o.cases.filter((c) => c.below != null || c.text);
90
+ let prev = o.scale.min;
91
+ const bands = zones.map((c) => { const to = c.below ?? o.scale.max, a = scalePos(prev, o.scale), b = scalePos(to, o.scale); prev = to; // rótulo da faixa: fora do ajuste de texto (não é .t) e só se a faixa tiver largura para ele
92
+ return `<span class="calc-band" style="left:${a}%;width:${Math.max(0, b - a)}%;background:${c.color ? `var(--${c.color})` : "var(--line)"}">${b - a >= 14 ? `<em class="f-label">${esc(c.text || "")}</em>` : ""}</span>`; }).join("");
93
+ const val = o.of ? res[o.of]?.value ?? null : r.value;
94
+ return `<div class="calc-scale"><div class="calc-track">${bands}</div><span class="calc-pointer" data-calc-pointer="${esc(o.name)}" style="left:${scalePos(val, o.scale)}%"></span></div>`;
95
+ })() : "";
96
+ const badge = r.text ? `<span class="calc-badge t f-display" data-calc-badge="${esc(o.name)}" style="${r.color ? `background:var(--${r.color})` : ""}">${esc(r.text)}</span>` : (o.cases.some((c) => c.text) ? `<span class="calc-badge t f-display" data-calc-badge="${esc(o.name)}"></span>` : "");
97
+ const value = o.fn || o.cases.some((c) => c.fn) ? `<div class="calc-out-v"><b data-calc-out="${esc(o.name)}">${esc(formatNum(r.value, o))}</b>${o.unit ? ` <small>${esc(o.unit)}</small>` : ""}</div>` : "";
98
+ const compare = `<small class="calc-compare" data-calc-compare="${esc(o.name)}" hidden></small>`;
99
+ const sweep = m.inputs.find(i => i.name === m.sweep);
100
+ const curve = sweep && o.fn ? C.curveData(m, {}, o.name) : null;
101
+ const chart = curve ? `<figure class="calc-curve"><svg viewBox="0 0 320 80" role="img" aria-label="${esc(o.label)} em função de ${esc(sweep.label)}"><path data-calc-curve="${esc(o.name)}" d="${curve.path}" fill="none" stroke="currentColor" stroke-width="3"/></svg><figcaption><span data-calc-axis="${esc(o.name)}">${esc(o.label)}: ${esc(formatNum(curve.min, o))} a ${esc(formatNum(curve.max, o))} ${esc(o.unit)} (escala automática)</span><br>${esc(sweep.label)}: ${esc(formatNum(sweep.min))} a ${esc(formatNum(sweep.max))} ${esc(sweep.unit)}</figcaption></figure>` : '';
102
+ return `<div class="calc-out"><div class="calc-l t f-label">${esc(o.label)}</div>${o.latex ? `<div class="calc-formula">${tex(o.latex, false)}</div>` : ""}${value}${badge}${scale}${chart}${compare}</div>`;
103
+ };
104
+ const payload = JSON.stringify(m).replace(/</g, "\\u003c");
105
+ const controls = `<div class="calc-tools">${m.scenarios.map((c, i) => `<button type="button" data-calc-scenario="${i}">${esc(c.label || `Cenário ${i + 1}`)}</button>`).join('')}<button type="button" data-calc-free>Explorar</button><button type="button" data-calc-freeze>Comparar com este</button><button type="button" data-calc-reset>Restaurar</button></div>`;
106
+ const prediction = m.prediction ? `<div class="calc-prediction"><span>${esc(m.prediction)}</span> <button type="button" data-calc-reveal>Ver resultado</button></div>` : '';
107
+ const explanation = `<p class="calc-explanation" data-calc-explanation aria-live="polite">${esc(m.explanation)}</p>`;
108
+ return `<div class="L-calc${m.outputs.length >= 4 ? ' calc-many' : ''}" style="--calc-columns:${m.outputs.length >= 7 ? 4 : 3}" data-calc>${head(s)}${m.illustrative ? '<small class="calc-disclaimer">Simulação ilustrativa</small>' : ''}${prediction}${controls}<div class="calc-body"><div class="calc-inputs">${m.inputs.map(input).join("")}</div><div class="calc-outputs">${m.outputs.map(output).join("")}</div></div>${explanation}<script type="application/json" class="calc-model">${payload}</script>${s.note ? `<div class="calc-note t f-body">${md(s.note)}</div>` : ""}</div>`;
109
+ }
110
+
111
+ // ------------------------------------------------------------------------------------------------ algo
112
+ import { traceAlgorithm, ALGO_CODE, ALGO_NAMES } from "./algo-trace.js";
113
+ import { tracePlan, traceHTML } from "./trace-view.js";
114
+ // Prever → rodar → explicar: `predict` vira o primeiro quadro (a turma aposta antes de ver), `explain` o último (o
115
+ // porquê, com a resposta da aposta em destaque). Os passos do algoritmo ficam no meio, renumerados.
116
+ function bookends(s, html) {
117
+ const p = s.predict == null || s.predict === "" ? null : typeof s.predict === "string" ? { question: s.predict } : s.predict;
118
+ const x = s.explain == null || s.explain === "" ? null : typeof s.explain === "string" ? { text: s.explain } : s.explain;
119
+ if (!p && !x) return html;
120
+ let out = html;
121
+ if (p) {
122
+ const opts = Array.isArray(p.options) ? p.options : [];
123
+ const pre = `<div class="algo-predict-in"><div class="algo-predict-tag t f-label">${esc(p.label || "Antes de rodar")}</div><div class="algo-predict-q t f-heading">${md(p.question || "")}</div>${opts.length ? `<ol class="algo-predict-opts">${opts.map((o, i) => `<li class="t f-body"><b>${String.fromCharCode(65 + i)}</b>${md(String(o))}</li>`).join("")}</ol>` : ""}</div>`;
124
+ out = out.replace(/data-lesson-panel="(\d+)"/g, (_, k) => `data-lesson-panel="${+k + 1}"`).replace(/(<section class="lesson-panel dyn-frame [^"]*?) active"/, "$1\"");
125
+ out = out.replace('<div class="dyn-frames">', `<div class="dyn-frames">${panel(0, pre, "algo-predict")}`);
126
+ }
127
+ const count = Number(out.match(/data-lesson-count="(\d+)"/)?.[1] || 0) + (p ? 1 : 0) + (x ? 1 : 0);
128
+ out = out.replace(/data-lesson-count="\d+"/, `data-lesson-count="${count}"`);
129
+ if (x) {
130
+ const ans = x.answer ?? p?.answer;
131
+ const post = `<div class="algo-explain-in"><div class="algo-predict-tag t f-label">${esc(x.label || "Por quê")}</div>${ans != null && ans !== "" ? `<div class="algo-explain-ans t f-heading">${md(String(ans))}</div>` : ""}<div class="algo-explain-text t f-body">${md(x.text || "")}</div></div>`;
132
+ const at = out.lastIndexOf("</section>");
133
+ if (at >= 0) out = out.slice(0, at + 10) + panel(count - 1, post, "algo-explain") + out.slice(at + 10);
134
+ }
135
+ return out;
136
+ }
137
+
138
+ export function algoHTML(s, ctx, head) {
139
+ return bookends(s, algoCore(s, ctx, head));
140
+ }
141
+ function algoCore(s, ctx, head) {
142
+ // programa do professor (program:) ou do catálogo (kmp, bfs…): execução rastreada genérica (src/pytrace.js)
143
+ const plan = tracePlan(s);
144
+ if (plan) return traceHTML(s, plan, ctx, head, panel);
145
+ const algorithm = String(s.algorithm || "bubble");
146
+ let steps;
147
+ try { steps = traceAlgorithm(algorithm, s.array || [5, 1, 4, 2, 8, 3], { target: s.target }); }
148
+ catch (e) { ctx?.warnings?.push(`algoritmo: ${e.message}`); return `<div class="L-algo">${head(s)}<div class="dyn-error f-body">${esc(e.message)}</div></div>`; }
149
+ // passos demais para clicar: some só com as comparações sem troca (o essencial fica)
150
+ const max = Number(s.maxSteps) || 90;
151
+ if (steps.length > max) steps = steps.filter((st, i) => i === 0 || i === steps.length - 1 || !(st.compare && !st.swap && st.write == null && st.found == null));
152
+ if (steps.length > max) { ctx?.warnings?.push(`algoritmo: ${steps.length} passos; use um vetor menor para caber no clique (até ${max})`); steps = steps.slice(0, max - 1).concat(steps.slice(-1)); }
153
+ const code = s.code ? String(s.code).split("\n") : ALGO_CODE[algorithm];
154
+ const top = Math.max(...steps.flatMap((st) => st.arr), 1);
155
+ const n = steps.length, search = ["linear", "binary"].includes(algorithm);
156
+ const bars = (st, prev) => {
157
+ const cmp = new Set(st.compare || []), sw = new Set(st.swap || []), sorted = new Set(st.sorted || []);
158
+ const [lo, hi] = st.range || [0, st.arr.length - 1];
159
+ return `<div class="algo-bars" style="--n:${st.arr.length}">${st.arr.map((v, i) => {
160
+ const from = st.swap && sw.has(i) ? (i === st.swap[0] ? st.swap[1] - st.swap[0] : st.swap[0] - st.swap[1]) : 0;
161
+ const cls = [cmp.has(i) && "cmp", sw.has(i) && "swp", st.write === i && "wr", st.pivot === i && "piv", sorted.has(i) && "ok", (i < lo || i > hi) && "out", st.found === i && "found"].filter(Boolean).join(" ");
162
+ return `<div class="algo-bar ${cls}" style="--h:${(v / top) * 100}%;--from:${from}"><span class="algo-val t f-display">${v}</span><i></i><span class="algo-idx t f-label">${i}</span></div>`;
163
+ }).join("")}</div>`;
164
+ };
165
+ const codeHTML = (line) => `<ol class="algo-code dyn-mono">${code.map((l, i) => `<li${i === line ? ' class="now"' : ""}>${esc(l)}</li>`).join("")}</ol>`;
166
+ const body = steps.map((st, k) => panel(k, `<div class="algo-row"><div class="algo-stage">${bars(st, steps[k - 1])}<div class="algo-caption t f-body">${md(st.text || "")}</div></div>
167
+ <aside class="algo-side"><div class="algo-name t f-label">${esc(s.name || ALGO_NAMES[algorithm] || algorithm)}${search && s.target != null ? ` · alvo <b>${esc(s.target)}</b>` : ""}</div>${codeHTML(st.line)}
168
+ <div class="algo-counters"><div><b class="t f-display">${st.comparisons}</b><span class="t f-label">comparações</span></div>${search ? "" : `<div><b class="t f-display">${st.swaps}</b><span class="t f-label">${algorithm === "merge" ? "escritas" : "trocas"}</span></div>`}<div><b class="t f-display">${k}/${n - 1}</b><span class="t f-label">passo</span></div></div>
169
+ </aside></div>`)).join("");
170
+ return `<div class="L-algo dyn" data-lesson="algo" data-lesson-count="${n}" data-autoplay-ms="${Number(s.speed) || 900}">${head(s)}<button type="button" class="algo-play" data-autoplay aria-pressed="false" title="Tocar sozinho (clique de novo para pausar)"><span class="algo-play-ic" aria-hidden="true"></span><span>Tocar</span></button><div class="dyn-frames">${body}</div></div>`;
171
+ }
package/src/library.js ADDED
@@ -0,0 +1,399 @@
1
+ // Biblioteca de apresentações: uma PASTA, sem banco de dados. Funciona igual no Windows do banco (só você)
2
+ // e no servidor (uma biblioteca por usuário). Dá para mexer pelo Explorer também: a biblioteca reflete.
3
+ //
4
+ // <biblioteca>/
5
+ // Palestras/ tópico = pasta (cor em .topico.json)
6
+ // E se o humano for o bug/ apresentação = pasta (igual a um .sagadeck extraído)
7
+ // E se o humano for o bug.yaml
8
+ // imagens/ …
9
+ // rascunho.yaml um .yaml solto no tópico também é uma apresentação
10
+ // rascunho.yaml .yaml solto na raiz = apresentação sem tópico
11
+ // Na raiz, pasta é SEMPRE tópico. "Nova sem tópico" vai para "Sem tópico"; importar sem tópico, para "Importados".
12
+ // .lixeira/ excluídas (30 dias), com .origem.json
13
+ // .cache/ capas geradas
14
+ //
15
+ // O id de uma apresentação é o caminho do .yaml dela, relativo à biblioteca, com "/" (ex.: "Palestras/X/X.yaml").
16
+ import fs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import YAML from "yaml";
20
+ import * as MASTER from "./master.js";
21
+ import { unpackDeck } from "./package.js";
22
+ import { writeDeckFile } from "./deck-file.js";
23
+
24
+ export const TRASH_DAYS = 30;
25
+ const TRASH = ".lixeira";
26
+ const TOPIC_META = ".topico.json";
27
+ const COLORS = ["#d33a2c", "#0f6cbd", "#e5a50a", "#8b5cf6", "#0e9f6e", "#e8590c", "#d6336c", "#495057"];
28
+
29
+ export function defaultLibraryRoot(env = process.env) {
30
+ return path.resolve(env.SAGADECK_HOME || path.join(os.homedir(), "sagadeck"));
31
+ }
32
+
33
+ // Onde uma apresentação NOVA é gravada: sempre na biblioteca. Um caminho dentro dela é respeitado; qualquer
34
+ // outro (pasta atual, pasta temporária, cópia do repositório) vira só o nome, em <biblioteca>/<tópico>/<nome>/<nome>.yaml.
35
+ // Decks espalhados por fora foram o que fez .js e imagens "sumirem". unique: não sobrescreve, usa "nome (2)".
36
+ export function newDeckPath(wanted, { root = defaultLibraryRoot(), topic = "Sem tópico", title, unique = false } = {}) {
37
+ root = path.resolve(root);
38
+ const abs = wanted ? path.resolve(root, String(wanted)) : "";
39
+ const rel = abs && path.relative(root, abs);
40
+ // caminho de Windows (C:\… ou \\servidor\…) num servidor Linux não é "absoluto" para o path de lá, mas também é
41
+ // de fora da biblioteca: vira só o nome, como em qualquer sistema
42
+ const foreign = process.platform !== "win32" && /^([A-Za-z]:[\\/]|\\\\)/.test(String(wanted || ""));
43
+ let dir, name, file;
44
+ if (rel && !foreign && !rel.startsWith("..") && !path.isAbsolute(rel) && /[\\/]/.test(String(wanted))) {
45
+ if (/\.ya?ml$/i.test(abs)) file = abs;
46
+ else { dir = abs; name = path.basename(abs); }
47
+ } else {
48
+ name = safeName(wanted ? String(wanted).split(/[\\/]/).pop().replace(/\.ya?ml$/i, "") : title || "Nova apresentação");
49
+ dir = path.join(root, safeName(topic, "Sem tópico"), name);
50
+ }
51
+ file ||= path.join(dir, name + ".yaml");
52
+ if (!unique || !fs.existsSync(file)) return file;
53
+ // pasta própria (<nome>/<nome>.yaml): numera a pasta e o arquivo; .yaml solto: só o arquivo
54
+ const own = path.basename(path.dirname(file)) === path.basename(file, path.extname(file));
55
+ const ext = path.extname(file), base = path.basename(file, ext);
56
+ for (let n = 2; ; n++) {
57
+ const f = own ? path.join(path.dirname(path.dirname(file)), `${base} (${n})`, `${base} (${n})${ext}`) : path.join(path.dirname(file), `${base} (${n})${ext}`);
58
+ if (!fs.existsSync(f)) return f;
59
+ }
60
+ }
61
+
62
+ const posix = (p) => p.split(path.sep).join("/");
63
+ const hidden = (name) => name.startsWith(".") || name.startsWith("~$");
64
+ const isYaml = (name) => /\.ya?ml$/i.test(name) && !hidden(name);
65
+
66
+ // nome de pasta/arquivo válido no Windows (e no Linux): sem <>:"/\|?*, sem ponto/espaço no fim
67
+ export function safeName(s, fallback = "Sem título") {
68
+ const clean = String(s || "").normalize("NFC").replace(/[<>:"/\\|?*\u0000-\u001f]/g, " ").replace(/\s+/g, " ").trim()
69
+ .replace(/[. ]+$/, "").slice(0, 80).trim();
70
+ return /^(con|prn|aux|nul|com\d|lpt\d)$/i.test(clean) || !clean ? fallback : clean;
71
+ }
72
+
73
+ function uniquePath(dir, name, ext = "") {
74
+ let p = path.join(dir, name + ext);
75
+ for (let n = 2; fs.existsSync(p); n++) p = path.join(dir, `${name} (${n})${ext}`);
76
+ return p;
77
+ }
78
+
79
+ export function openLibrary(root) {
80
+ root = path.resolve(root);
81
+ fs.mkdirSync(root, { recursive: true });
82
+
83
+ // caminho absoluto de um id, sem sair da biblioteca
84
+ function resolveId(id) {
85
+ const abs = path.resolve(root, ...String(id || "").split("/"));
86
+ const rel = path.relative(root, abs);
87
+ if (!id || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error("apresentação fora da biblioteca");
88
+ return abs;
89
+ }
90
+ const idOf = (abs) => posix(path.relative(root, abs));
91
+
92
+ // a apresentação principal de uma pasta: manifesto do .sagadeck, ou o .yaml com o nome da pasta, ou o 1º
93
+ function mainYaml(dir) {
94
+ const files = fs.readdirSync(dir).filter(isYaml);
95
+ if (!files.length) return null;
96
+ try {
97
+ const m = JSON.parse(fs.readFileSync(path.join(dir, "sagadeck.json"), "utf8"));
98
+ if (m.main && files.includes(m.main)) return path.join(dir, m.main);
99
+ } catch {}
100
+ const same = files.find((f) => f.replace(/\.ya?ml$/i, "") === path.basename(dir));
101
+ return path.join(dir, same || files.sort()[0]);
102
+ }
103
+
104
+ function deckInfo(file, topic) {
105
+ const st = fs.statSync(file);
106
+ let spec = {};
107
+ try { spec = YAML.parse(fs.readFileSync(file, "utf8")) || {}; } catch {}
108
+ const own = idOf(file).split("/").length === 3;
109
+ return {
110
+ id: idOf(file), topic: topic || "", title: String(spec.title || path.basename(file).replace(/\.ya?ml$/i, "")),
111
+ slides: Array.isArray(spec.slides) ? spec.slides.length : 0, theme: spec.theme || "", edited: st.mtimeMs,
112
+ folder: own, // true = pasta própria (com imagens etc.); false = .yaml solto
113
+ };
114
+ }
115
+
116
+ function topicMeta(dir) {
117
+ try { return JSON.parse(fs.readFileSync(path.join(dir, TOPIC_META), "utf8")); } catch { return {}; }
118
+ }
119
+
120
+ function list() {
121
+ // a pasta pode ter sido apagada por fora (Explorer, limpeza) com o servidor no ar: recria
122
+ fs.mkdirSync(root, { recursive: true });
123
+ purgeOld();
124
+ const topics = [], decks = [];
125
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
126
+ if (hidden(e.name)) continue;
127
+ const abs = path.join(root, e.name);
128
+ if (e.isFile() && isYaml(e.name)) { decks.push(deckInfo(abs, "")); continue; }
129
+ if (!e.isDirectory()) continue;
130
+ const meta = topicMeta(abs);
131
+ const mine = [];
132
+ for (const d of fs.readdirSync(abs, { withFileTypes: true })) {
133
+ if (hidden(d.name)) continue;
134
+ const p = path.join(abs, d.name);
135
+ if (d.isFile() && isYaml(d.name)) mine.push(deckInfo(p, e.name));
136
+ else if (d.isDirectory()) { const m = mainYaml(p); if (m) mine.push(deckInfo(m, e.name)); }
137
+ }
138
+ decks.push(...mine);
139
+ topics.push({ id: e.name, name: e.name, color: meta.color || COLORS[topics.length % COLORS.length], count: mine.length, created: meta.created || 0 });
140
+ }
141
+ topics.sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
142
+ return { root, topics, decks, trash: listTrash() };
143
+ }
144
+
145
+ // ---- tópicos ----
146
+ function topicDir(id) {
147
+ const dir = resolveId(id);
148
+ if (path.dirname(dir) !== root) throw new Error("tópico inválido");
149
+ return dir;
150
+ }
151
+ function createTopic(name, color) {
152
+ const n = safeName(name, "Novo tópico");
153
+ fs.mkdirSync(root, { recursive: true });
154
+ // Repeated requests (including Enter + click) reuse the topic, never multiply folders.
155
+ const existing = fs.readdirSync(root, { withFileTypes: true }).find(e => e.isDirectory() && !hidden(e.name) && e.name.normalize("NFC").toLocaleLowerCase("pt-BR") === n.toLocaleLowerCase("pt-BR"));
156
+ if (existing) return existing.name;
157
+ const dir = uniquePath(root, n);
158
+ fs.mkdirSync(dir, { recursive: true });
159
+ fs.writeFileSync(path.join(dir, TOPIC_META), JSON.stringify({ color: color || COLORS[list().topics.length % COLORS.length], created: Date.now() }));
160
+ return path.basename(dir);
161
+ }
162
+ function updateTopic(id, { name, color } = {}) {
163
+ let dir = topicDir(id);
164
+ if (color) fs.writeFileSync(path.join(dir, TOPIC_META), JSON.stringify({ ...topicMeta(dir), color }));
165
+ if (name && safeName(name) !== path.basename(dir)) {
166
+ const to = uniquePath(root, safeName(name));
167
+ fs.renameSync(dir, to);
168
+ dir = to;
169
+ }
170
+ return path.basename(dir);
171
+ }
172
+ function deleteTopic(id) {
173
+ const dir = topicDir(id);
174
+ const left = fs.readdirSync(dir).filter((n) => !hidden(n));
175
+ if (left.length) throw new Error("o tópico não está vazio: mova ou exclua as apresentações antes");
176
+ fs.rmSync(dir, { recursive: true, force: true });
177
+ }
178
+
179
+ // ---- apresentações ----
180
+ const ensureTopic = (name) => (fs.existsSync(path.join(root, name)) ? name : createTopic(name));
181
+
182
+ function createDeck(topic, spec) {
183
+ const title = spec?.title || "Nova apresentação";
184
+ const base = topicDir(topic || ensureTopic("Sem tópico"));
185
+ const dir = uniquePath(base, safeName(title));
186
+ fs.mkdirSync(dir, { recursive: true });
187
+ const file = path.join(dir, path.basename(dir) + ".yaml");
188
+ fs.writeFileSync(file, YAML.stringify(spec, { indent: 2, lineWidth: 0 }));
189
+ return idOf(file);
190
+ }
191
+
192
+ // pasta que "é" a apresentação (para mover/duplicar/excluir): a pasta própria, ou só o .yaml solto
193
+ // o id diz onde está: "x.yaml" (raiz), "Tópico/x.yaml" (solto no tópico), "Tópico/Pasta/x.yaml" (pasta própria)
194
+ function unitOf(id) {
195
+ const file = resolveId(id);
196
+ if (!fs.existsSync(file) || !isYaml(path.basename(file))) throw new Error("apresentação não encontrada");
197
+ const parts = idOf(file).split("/");
198
+ if (parts.length > 3) throw new Error("apresentação fora da estrutura da biblioteca");
199
+ const info = deckInfo(file, parts.length > 1 ? parts[0] : "");
200
+ return { file, unit: parts.length === 3 ? path.dirname(file) : file, info };
201
+ }
202
+
203
+ function moveDeck(id, topic) {
204
+ const { file, unit } = unitOf(id);
205
+ if (!topic && path.dirname(unit) === root) return id; // .yaml solto na raiz: já está sem tópico
206
+ // sem tópico = a pasta "Sem tópico": na raiz só ficam tópicos, nunca a pasta de uma apresentação
207
+ const dest = topicDir(topic || ensureTopic("Sem tópico"));
208
+ if (path.dirname(unit) === dest) return id;
209
+ const ext = unit === file ? path.extname(file) : "";
210
+ const to = uniquePath(dest, path.basename(unit, ext), ext);
211
+ fs.renameSync(unit, to);
212
+ return idOf(unit === file ? to : path.join(to, path.basename(file)));
213
+ }
214
+
215
+ function renameDeck(id, title) {
216
+ const { file, unit } = unitOf(id);
217
+ const spec = YAML.parse(fs.readFileSync(file, "utf8")) || {};
218
+ spec.title = title;
219
+ writeDeckFile(file, spec); // só o título muda no arquivo
220
+ if (unit === file) return id; // .yaml solto: só o título
221
+ const to = uniquePath(path.dirname(unit), safeName(title));
222
+ if (path.basename(to) === path.basename(unit)) return id;
223
+ fs.renameSync(unit, to);
224
+ // o .yaml com o nome da pasta acompanha a pasta
225
+ let yaml = path.join(to, path.basename(file));
226
+ if (path.basename(file).replace(/\.ya?ml$/i, "") === path.basename(unit)) {
227
+ const renamed = path.join(to, path.basename(to) + path.extname(file));
228
+ fs.renameSync(yaml, renamed);
229
+ yaml = renamed;
230
+ }
231
+ return idOf(yaml);
232
+ }
233
+
234
+ function duplicateDeck(id) {
235
+ const { file, unit, info } = unitOf(id);
236
+ const title = `${info.title} (cópia)`;
237
+ const ext = unit === file ? path.extname(file) : "";
238
+ const to = uniquePath(path.dirname(unit), safeName(title), ext);
239
+ fs.cpSync(unit, to, { recursive: true });
240
+ let yaml = unit === file ? to : path.join(to, path.basename(file));
241
+ if (unit !== file && path.basename(file).replace(/\.ya?ml$/i, "") === path.basename(unit)) {
242
+ const renamed = path.join(to, path.basename(to) + path.extname(file));
243
+ fs.renameSync(yaml, renamed);
244
+ yaml = renamed;
245
+ }
246
+ const spec = YAML.parse(fs.readFileSync(yaml, "utf8")) || {};
247
+ spec.title = title;
248
+ writeDeckFile(yaml, spec);
249
+ return idOf(yaml);
250
+ }
251
+
252
+ // ---- lixeira ----
253
+ function trashDeck(id) {
254
+ const { file, unit, info } = unitOf(id);
255
+ const tdir = path.join(root, TRASH);
256
+ fs.mkdirSync(tdir, { recursive: true });
257
+ const slot = path.join(tdir, `${Date.now()}-${path.basename(unit)}`);
258
+ fs.mkdirSync(slot);
259
+ fs.renameSync(unit, path.join(slot, path.basename(unit)));
260
+ fs.writeFileSync(path.join(slot, ".origem.json"), JSON.stringify({
261
+ topic: info.topic, title: info.title, deleted: Date.now(), yaml: path.relative(unit === file ? path.dirname(file) : unit, file),
262
+ unit: path.basename(unit), slides: info.slides,
263
+ }));
264
+ return path.basename(slot);
265
+ }
266
+ function listTrash() {
267
+ const tdir = path.join(root, TRASH);
268
+ if (!fs.existsSync(tdir)) return [];
269
+ return fs.readdirSync(tdir).map((slot) => {
270
+ try { return { id: slot, ...JSON.parse(fs.readFileSync(path.join(tdir, slot, ".origem.json"), "utf8")) }; } catch { return null; }
271
+ }).filter(Boolean).sort((a, b) => b.deleted - a.deleted);
272
+ }
273
+ function restoreDeck(slotId) {
274
+ const slot = path.join(root, TRASH, path.basename(slotId));
275
+ const o = JSON.parse(fs.readFileSync(path.join(slot, ".origem.json"), "utf8"));
276
+ const dest = o.topic && fs.existsSync(path.join(root, o.topic)) ? path.join(root, o.topic) : o.topic ? path.join(root, createTopic(o.topic)) : root;
277
+ const isFile = /\.ya?ml$/i.test(o.unit);
278
+ const ext = isFile ? path.extname(o.unit) : "";
279
+ const to = uniquePath(dest, path.basename(o.unit, ext), ext);
280
+ fs.renameSync(path.join(slot, o.unit), to);
281
+ fs.rmSync(slot, { recursive: true, force: true });
282
+ return idOf(isFile ? to : path.join(to, o.yaml));
283
+ }
284
+ function purgeDeck(slotId) {
285
+ fs.rmSync(path.join(root, TRASH, path.basename(slotId)), { recursive: true, force: true });
286
+ }
287
+ // esvaziar a lixeira: tudo de uma vez (a pessoa confirma antes, no Studio)
288
+ function emptyTrash() {
289
+ const all = listTrash();
290
+ all.forEach((t) => purgeDeck(t.id));
291
+ return all.length;
292
+ }
293
+ function purgeOld(now = Date.now()) {
294
+ for (const t of listTrash()) if (now - t.deleted > TRASH_DAYS * 864e5) purgeDeck(t.id);
295
+ }
296
+
297
+ // ---- importar .sagadeck / .zip ----
298
+ async function importPackage(buffer, topic, name = "Importada") {
299
+ const base = topicDir(topic || ensureTopic("Importados"));
300
+ const dir = uniquePath(base, safeName(name.replace(/\.(sagadeck|zip)$/i, "")));
301
+ const { file } = await unpackDeck(buffer, dir);
302
+ return idOf(file);
303
+ }
304
+
305
+ // .pptx (PowerPoint, Google Slides, Keynote exportado) vira uma apresentação da biblioteca, fiel ao original
306
+ // (src/import): cada slide na mesma posição, mídias em imagens/original/, cópia e fotos do original em original/
307
+ async function importOffice(buffer, topic, name = "Importada.pptx", opts = {}) {
308
+ if (!/\.pptx$/i.test(name)) throw new Error("Por enquanto a importação lê .pptx (o PowerPoint salva em .pptx; o Google Slides e o Keynote exportam).");
309
+ const { importToDir } = await import("./import/index.js");
310
+ const base = topicDir(topic || ensureTopic("Importados"));
311
+ const dir = uniquePath(base, safeName(opts.title || name.replace(/\.pptx$/i, "")));
312
+ const r = await importToDir(buffer, dir, { fileName: path.basename(name), snapshots: opts.snapshots !== false, log: opts.log });
313
+ if (opts.title) r.spec.title = opts.title;
314
+ const file = path.join(dir, path.basename(dir) + ".yaml");
315
+ fs.writeFileSync(file, YAML.stringify(r.spec, { indent: 2, lineWidth: 0 }));
316
+ return { id: idOf(file), file, slides: r.spec.slides.length, snapshots: r.snapshots, snapBy: r.snapBy, converted: r.converted, warnings: r.warnings };
317
+ }
318
+
319
+ // ---- estilos da pessoa (tema + mestre), reutilizáveis: <biblioteca>/.estilos/<slug>/estilo.yaml + imagens
320
+ const stylesRoot = path.join(root, ".estilos");
321
+ const slug = (n) => safeName(String(n || "estilo")).toLowerCase().replace(/\s+/g, "-");
322
+ // estilo padrão (Brand Kit): toda apresentação nova (em branco ou com IA, sem tema escolhido) já nasce com ele.
323
+ // Guardado na própria biblioteca (.estilos/padrao.json): vale para quem usar esta biblioteca, em qualquer máquina.
324
+ const defaultFile = path.join(stylesRoot, "padrao.json");
325
+ function defaultStyle() {
326
+ try { const id = JSON.parse(fs.readFileSync(defaultFile, "utf8")).id; return id && fs.existsSync(path.join(stylesRoot, slug(id), "estilo.yaml")) ? slug(id) : null; } catch { return null; }
327
+ }
328
+ function setDefaultStyle(id) {
329
+ if (!id) { fs.rmSync(defaultFile, { force: true }); return null; }
330
+ loadStyle(id); // existe?
331
+ fs.mkdirSync(stylesRoot, { recursive: true });
332
+ fs.writeFileSync(defaultFile, JSON.stringify({ id: slug(id) }));
333
+ return slug(id);
334
+ }
335
+ // apresentação nova: com o padrão, se houver e se ela ainda não tem estilo próprio
336
+ function withDefaultStyle(spec, deckDir) {
337
+ const id = defaultStyle();
338
+ if (!id || spec?.master || spec?.style) return spec;
339
+ try { return applyStyleTo(spec, deckDir, id); } catch { return spec; }
340
+ }
341
+
342
+ function listStyles() {
343
+ if (!fs.existsSync(stylesRoot)) return [];
344
+ return fs.readdirSync(stylesRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && fs.existsSync(path.join(stylesRoot, d.name, "estilo.yaml"))).map((d) => {
345
+ try { const st = YAML.parse(fs.readFileSync(path.join(stylesRoot, d.name, "estilo.yaml"), "utf8")); return { id: d.name, name: st.name || d.name, default: d.name === defaultStyle(), from: st.from || null, accent: st.theme?.colors?.accent || null, paper: st.theme?.colors?.paper || null }; }
346
+ catch { return null; }
347
+ }).filter(Boolean).sort((x, y) => x.name.localeCompare(y.name, "pt-BR"));
348
+ }
349
+ function saveStyle(style, fromDir, { replace = false } = {}) {
350
+ const { masterImages } = MASTER;
351
+ let id = slug(style.name);
352
+ if (!replace) { const base = id; for (let n = 2; fs.existsSync(path.join(stylesRoot, id)); n++) id = `${base}-${n}`; }
353
+ const dir = path.join(stylesRoot, id);
354
+ fs.mkdirSync(dir, { recursive: true });
355
+ for (const rel of masterImages(style.master)) {
356
+ const src = path.resolve(fromDir, rel);
357
+ if (!src.startsWith(path.resolve(fromDir)) || !fs.existsSync(src)) continue;
358
+ const dst = path.join(dir, ...rel.split("/"));
359
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
360
+ fs.copyFileSync(src, dst);
361
+ }
362
+ fs.writeFileSync(path.join(dir, "estilo.yaml"), YAML.stringify(style, { indent: 2, lineWidth: 0 }));
363
+ return { id, name: style.name };
364
+ }
365
+ function loadStyle(id) {
366
+ const dir = path.join(stylesRoot, slug(id));
367
+ if (!dir.startsWith(stylesRoot) || !fs.existsSync(path.join(dir, "estilo.yaml"))) throw new Error("estilo não encontrado");
368
+ return { style: YAML.parse(fs.readFileSync(path.join(dir, "estilo.yaml"), "utf8")), dir, id: slug(id) };
369
+ }
370
+ // aplica num deck (spec + pasta): as imagens da moldura vão para imagens/estilo/<id>/ e o deck fica portátil
371
+ function applyStyleTo(spec, deckDir, id) {
372
+ const { style, dir } = loadStyle(id);
373
+ const { masterImages } = MASTER;
374
+ const map = new Map();
375
+ for (const rel of masterImages(style.master)) {
376
+ const src = path.join(dir, ...rel.split("/"));
377
+ if (!fs.existsSync(src)) continue;
378
+ const to = `imagens/estilo/${slug(id)}/${path.basename(rel)}`;
379
+ const dst = path.join(deckDir, ...to.split("/"));
380
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
381
+ fs.copyFileSync(src, dst);
382
+ map.set(rel, to);
383
+ }
384
+ const master = structuredClone(style.master);
385
+ const fix = (list) => (list || []).forEach((e) => { if (e.image && map.has(e.image)) e.image = map.get(e.image); if (e.drawing) e.drawing = String(e.drawing).replace(/href="media:([^"]+)"/g, (m, r) => `href="media:${map.get(r) || r}"`); });
386
+ fix(master.elements); fix(master.cover);
387
+ return { ...spec, theme: style.theme, master, footer: false, style: { id: slug(id), name: style.name } };
388
+ }
389
+
390
+ return {
391
+ listStyles, saveStyle, loadStyle, applyStyleTo, defaultStyle, setDefaultStyle, withDefaultStyle,
392
+ importOffice,
393
+ root, list, resolveId, idOf,
394
+ createTopic, updateTopic, deleteTopic,
395
+ createDeck, moveDeck, renameDeck, duplicateDeck,
396
+ trashDeck, restoreDeck, purgeDeck, purgeOld, listTrash, emptyTrash,
397
+ importPackage,
398
+ };
399
+ }