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,371 @@
1
+ // sagadeck · Servidor MCP (Model Context Protocol) para IDEs Agênticos
2
+ // Permite que Cursor, Claude Code, Windsurf, Cline, Roo Code etc. descubram e invoquem
3
+ // ferramentas do sagadeck via protocolo JSON-RPC 2.0 padrão sobre stdio.
4
+
5
+ import readline from "node:readline";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import YAML from "yaml";
9
+ import { loadSpec, buildHTML, buildFile } from "../build.js";
10
+ import { autofixDeck } from "../fiscal/autofix.js";
11
+ import { THEMES } from "../themes.js";
12
+ import { LAYOUTS } from "../layouts.js";
13
+ import { createStudioServer } from "../studio/server.js";
14
+ import { newDeckPath } from "../library.js";
15
+ import { writeDeckFile } from "../deck-file.js";
16
+
17
+ const TOOLS = [
18
+ {
19
+ name: "sagadeck_read_deck",
20
+ description: "Lê e analisa a estrutura de um arquivo de apresentação Sagadeck (.yaml), retornando título, tema, slides e notas.",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: {
24
+ path: { type: "string", description: "Caminho do arquivo .yaml da apresentação" },
25
+ },
26
+ required: ["path"],
27
+ },
28
+ },
29
+ {
30
+ name: "sagadeck_inspect_deck",
31
+ description: "Fiscal de layout: inspeciona o deck procurando sobreposição de elementos, transgressão de margens seguras (120px), texto estourado e excesso de palavras (anti-sono).",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ path: { type: "string", description: "Caminho do arquivo .yaml a ser fiscalizado" },
36
+ },
37
+ required: ["path"],
38
+ },
39
+ },
40
+ {
41
+ name: "sagadeck_autofix_deck",
42
+ description: "Auto-cura do Sagadeck: analisa a geometria dos slides e repara automaticamente sobreposições, quebras de margem e excessos de texto no arquivo YAML.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ path: { type: "string", description: "Caminho do arquivo .yaml" },
47
+ out: { type: "string", description: "Caminho de saída opcional (sobrescreve se omitido)" },
48
+ },
49
+ required: ["path"],
50
+ },
51
+ },
52
+ {
53
+ name: "sagadeck_build_html",
54
+ description: "Compila o arquivo .yaml em uma apresentação .html standalone interativa, com suporte a modo apresentador (P), animações e offline.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ path: { type: "string", description: "Caminho do arquivo .yaml" },
59
+ out: { type: "string", description: "Caminho de saída do .html (opcional)" },
60
+ },
61
+ required: ["path"],
62
+ },
63
+ },
64
+ {
65
+ name: "sagadeck_text_to_visual",
66
+ description: "Transforma texto bruto, notas de reunião ou tópicos em um diagrama visual inteligente (estilo Napkin AI: steps, stats, compare, cards, statement). Detecta o padrão semântico ideal sem alucinação.",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ text: { type: "string", description: "Texto bruto, lista de tópicos ou notas a converter em diagrama" },
71
+ title: { type: "string", description: "Título opcional para o slide" },
72
+ kicker: { type: "string", description: "Kicker/chapeuzinho opcional (ex: PROCESSO, MÉTRICAS)" },
73
+ theme: { type: "string", description: "Tema do deck se for gerar apresentação completa" },
74
+ tone: { type: "string", enum: ["dark", "light", "accent"], description: "Tom visual do slide" },
75
+ },
76
+ required: ["text"],
77
+ },
78
+ },
79
+ {
80
+ name: "sagadeck_scaffold_deck",
81
+ description: "Gera um esqueleto narrativo pronto de alta qualidade (cover -> statement -> stats -> steps -> cards -> end) para preenchimento direto. Economiza até 80% dos tokens de saída da IA.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ path: { type: "string", description: "Nome da apresentação (ou caminho dentro da biblioteca SAGADECK_HOME/~/sagadeck). Toda apresentação é gravada na biblioteca: caminho de fora vira só o nome, e nada é sobrescrito" },
86
+ title: { type: "string", description: "Título provisório da apresentação" },
87
+ theme: {
88
+ type: "string",
89
+ enum: ["prata", "rabisco", "oceano", "pop", "aurora", "sinal", "editorial", "noite"],
90
+ description: "Tema visual (padrão: prata)",
91
+ },
92
+ type: {
93
+ type: "string",
94
+ enum: ["keynote", "pitch", "palestra"],
95
+ description: "Tipo narrativo do esqueleto (padrão: keynote)",
96
+ },
97
+ author: { type: "string", description: "Nome do autor ou palestrante" },
98
+ },
99
+ required: ["path"],
100
+ },
101
+ },
102
+ {
103
+ name: "sagadeck_create_deck",
104
+ description: "Cria uma nova apresentação Sagadeck com tema e estrutura inicial.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ path: { type: "string", description: "Nome da apresentação (ou caminho dentro da biblioteca SAGADECK_HOME/~/sagadeck). Toda apresentação é gravada na biblioteca: caminho de fora vira só o nome, e nada é sobrescrito" },
109
+ title: { type: "string", description: "Título da apresentação" },
110
+ theme: {
111
+ type: "string",
112
+ enum: ["sinal", "prata", "rabisco", "oceano", "pop", "aurora", "editorial", "noite", "bauhaus", "terminal", "jornal"],
113
+ description: "Tema visual do Sagadeck (ex.: prata para Apple Keynote, rabisco para artesanal, oceano para azul elétrico)",
114
+ },
115
+ author: { type: "string", description: "Nome do autor ou palestrante" },
116
+ },
117
+ required: ["path", "title"],
118
+ },
119
+ },
120
+ {
121
+ name: "sagadeck_web_search",
122
+ description: "Pesquisa na web via DuckDuckGo (sem bloqueio de CAPTCHA/Google bot) para enriquecer o conteúdo dos slides com dados reais, estatísticas e fontes confiáveis.",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ query: { type: "string", description: "Termo de busca na web" },
127
+ limit: { type: "number", description: "Número de resultados desejados (padrão: 5)" },
128
+ },
129
+ required: ["query"],
130
+ },
131
+ },
132
+ {
133
+ name: "sagadeck_launch_studio",
134
+ description: "Inicia o SagaDeck Studio (página web estilo PowerPoint com canvas visual, edição WYSIWYG e chat lateral com IA).",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ path: { type: "string", description: "Caminho do arquivo .yaml a carregar no Studio" },
139
+ port: { type: "number", description: "Porta HTTP (padrão: 3000)" },
140
+ },
141
+ },
142
+ },
143
+ ];
144
+
145
+ export { TOOLS, handleToolCall };
146
+ export function runMCPServer() {
147
+ const rl = readline.createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout,
150
+ terminal: false,
151
+ });
152
+
153
+ const send = (obj) => {
154
+ process.stdout.write(JSON.stringify(obj) + "\n");
155
+ };
156
+
157
+ rl.on("line", async (line) => {
158
+ const trimmed = line.trim();
159
+ if (!trimmed) return;
160
+
161
+ let req;
162
+ try {
163
+ req = JSON.parse(trimmed);
164
+ } catch {
165
+ return;
166
+ }
167
+
168
+ const { id, method, params } = req;
169
+
170
+ if (method === "initialize") {
171
+ send({
172
+ jsonrpc: "2.0",
173
+ id,
174
+ result: {
175
+ protocolVersion: "2024-11-05",
176
+ serverInfo: { name: "sagadeck-mcp", version: "1.1.0" },
177
+ capabilities: { tools: {} },
178
+ },
179
+ });
180
+ return;
181
+ }
182
+
183
+ if (method === "tools/list") {
184
+ send({
185
+ jsonrpc: "2.0",
186
+ id,
187
+ result: { tools: TOOLS },
188
+ });
189
+ return;
190
+ }
191
+
192
+ if (method === "tools/call") {
193
+ const toolName = params?.name;
194
+ const args = params?.arguments || {};
195
+
196
+ try {
197
+ const result = await handleToolCall(toolName, args);
198
+ send({
199
+ jsonrpc: "2.0",
200
+ id,
201
+ result: {
202
+ content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
203
+ },
204
+ });
205
+ } catch (err) {
206
+ send({
207
+ jsonrpc: "2.0",
208
+ id,
209
+ error: { code: -32000, message: err.message },
210
+ });
211
+ }
212
+ return;
213
+ }
214
+
215
+ // Ping / notifications
216
+ if (method === "ping") {
217
+ send({ jsonrpc: "2.0", id, result: {} });
218
+ }
219
+ });
220
+ }
221
+
222
+ async function handleToolCall(name, args) {
223
+ switch (name) {
224
+ case "sagadeck_read_deck": {
225
+ const spec = loadSpec(path.resolve(args.path));
226
+ return {
227
+ title: spec.title,
228
+ theme: spec.theme,
229
+ duration: spec.duration,
230
+ slidesCount: spec.slides?.length || 0,
231
+ slides: spec.slides,
232
+ };
233
+ }
234
+
235
+ case "sagadeck_inspect_deck": {
236
+ const spec = loadSpec(path.resolve(args.path));
237
+ const res = buildHTML(spec);
238
+ const warnings = res.warnings || [];
239
+ const issues = [];
240
+ // Anti-sleep & boundary checks
241
+ spec.slides.forEach((s, idx) => {
242
+ const words = (JSON.stringify(s).match(/\b\w+\b/g) || []).length;
243
+ if (words > 40) {
244
+ issues.push({ slide: idx + 1, kind: "anti-sono", detail: `${words} palavras (limite 40)` });
245
+ }
246
+ if (s.title && s.title.length > 55 && !s.titleSize) {
247
+ issues.push({ slide: idx + 1, kind: "titulo-longo", detail: "Título longo sem titleSize explícito (risco de sobreposição)" });
248
+ }
249
+ });
250
+ return {
251
+ clean: warnings.length === 0 && issues.length === 0,
252
+ warnings,
253
+ issues,
254
+ };
255
+ }
256
+
257
+ case "sagadeck_autofix_deck": {
258
+ const targetPath = path.resolve(args.path);
259
+ const spec = loadSpec(targetPath);
260
+ const fixResult = autofixDeck(spec);
261
+ const outPath = args.out ? path.resolve(args.out) : targetPath;
262
+ writeDeckFile(outPath, fixResult.spec);
263
+ return {
264
+ ok: true,
265
+ savedTo: outPath,
266
+ modifiedSlidesCount: fixResult.modifiedSlidesCount,
267
+ actions: fixResult.actions,
268
+ };
269
+ }
270
+
271
+ case "sagadeck_build_html": {
272
+ const targetPath = path.resolve(args.path);
273
+ const outPath = args.out ? path.resolve(args.out) : targetPath.replace(/\.ya?ml$/i, ".html");
274
+ const res = buildFile(targetPath, outPath);
275
+ return {
276
+ ok: true,
277
+ htmlPath: outPath,
278
+ slidesCount: res.meta.slides.length,
279
+ theme: res.theme.name,
280
+ };
281
+ }
282
+
283
+ case "sagadeck_text_to_visual": {
284
+ const { textToVisualSlide } = await import("../diagram/napkin.js");
285
+ const YAML = (await import("yaml")).default;
286
+ const res = textToVisualSlide(args.text, {
287
+ title: args.title,
288
+ kicker: args.kicker,
289
+ theme: args.theme,
290
+ tone: args.tone,
291
+ });
292
+ return {
293
+ ok: true,
294
+ detectedType: res.detectedType,
295
+ confidence: res.confidence,
296
+ rationale: res.rationale,
297
+ slide: res.slide,
298
+ slideYaml: YAML.stringify(res.slide, { indent: 2 }),
299
+ };
300
+ }
301
+
302
+ case "sagadeck_scaffold_deck": {
303
+ // sempre na biblioteca, sem sobrescrever (ver newDeckPath)
304
+ const targetPath = newDeckPath(args.path, { title: args.title, unique: true });
305
+ const { generateScaffold } = await import("../templates/scaffold.js");
306
+ const YAML = (await import("yaml")).default;
307
+ const spec = generateScaffold({
308
+ title: args.title || "Nova Apresentação",
309
+ theme: args.theme || "prata",
310
+ type: args.type || "keynote",
311
+ author: args.author || "Seu Nome",
312
+ });
313
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
314
+ fs.writeFileSync(targetPath, YAML.stringify(spec, { indent: 2 }), "utf8");
315
+ return {
316
+ ok: true,
317
+ path: targetPath,
318
+ theme: spec.theme,
319
+ slidesCount: spec.slides.length,
320
+ message: `Esqueleto narrativo gerado com sucesso (${spec.slides.length} slides). Economiza 80% dos tokens da IA.`,
321
+ };
322
+ }
323
+
324
+ case "sagadeck_create_deck": {
325
+ const targetPath = newDeckPath(args.path, { title: args.title, unique: true });
326
+ const content = {
327
+ title: args.title || "Nova Apresentação",
328
+ theme: args.theme || "sinal",
329
+ author: args.author || "Autor",
330
+ duration: 15,
331
+ slides: [
332
+ {
333
+ layout: "cover",
334
+ title: args.title || "Nova Apresentação",
335
+ subtitle: "Apresentação estruturada com Sagadeck",
336
+ author: args.author || "Autor",
337
+ },
338
+ {
339
+ layout: "statement",
340
+ kicker: "Ponto Principal",
341
+ text: "Uma mensagem clara e memorável por slide.",
342
+ tone: "accent",
343
+ },
344
+ ],
345
+ };
346
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
347
+ fs.writeFileSync(targetPath, YAML.stringify(content, { indent: 2 }), "utf8");
348
+ return { ok: true, created: targetPath };
349
+ }
350
+
351
+ case "sagadeck_launch_studio": {
352
+ const port = args.port || 3000;
353
+ const server = createStudioServer(args.path, { port });
354
+ server.listen(port, "0.0.0.0");
355
+ return { ok: true, url: `http://localhost:${port}`, message: "Studio ativo" };
356
+ }
357
+
358
+ case "sagadeck_web_search": {
359
+ const { searchDuckDuckGo } = await import("../research/duckduckgo.js");
360
+ const results = await searchDuckDuckGo(args.query, { limit: args.limit || 5 });
361
+ return {
362
+ query: args.query,
363
+ count: results.length,
364
+ results,
365
+ };
366
+ }
367
+
368
+ default:
369
+ throw new Error(`Ferramenta desconhecida: ${name}`);
370
+ }
371
+ }
package/src/meeting.js ADDED
@@ -0,0 +1,42 @@
1
+ import {esc} from './markup.js';
2
+
3
+ export function meetingSpec(raw, audience = raw?.audience || 'organization') {
4
+ if(!Array.isArray(raw?.slides))return raw;
5
+ if(audience!=='participants'&&!raw.slides.some(s=>(s.layout==='poll'&&s.manual)||s.audience==='organization'))return raw;
6
+ const spec=structuredClone(raw),decisions=spec.slides.filter(s=>s.layout==='poll'&&s.manual&&s.decision?.approved),approved=new Map(decisions.map(s=>[s.id,s.options?.[s.decision.selected]])),numbers=new Map(decisions.map(s=>[s.id,s.values?.[s.decision.selected]]));
7
+ for(const s of spec.slides)for(const input of Object.values(s.inputs||{}))if(input&&typeof input==='object'&&Number.isFinite(numbers.get(input.decision)))input.value=numbers.get(input.decision);
8
+ const known=new Set(spec.slides.filter(s=>s.layout==='poll'&&s.manual).map(s=>s.id));
9
+ const replace=v=>typeof v==='string'?v.replace(/\{\{decision:([^}]+)\}\}/g,(literal,id)=>known.has(id)?approved.get(id)||'Em definição':literal):Array.isArray(v)?v.map(replace):v&&typeof v==='object'?Object.fromEntries(Object.entries(v).map(([k,x])=>[k,replace(x)])):v;
10
+ spec.slides=spec.slides.flatMap(s=>{
11
+ if(audience!=='participants')return [s];
12
+ if(s.audience==='organization')return [];
13
+ if(s.layout==='poll'&&s.manual){if(!s.decision?.approved)return [];return [{layout:'statement',id:s.id,title:s.participantTitle||s.question,text:s.options[s.decision.selected],kicker:'REGRA APROVADA',tone:s.tone,notes:s.participantNotes||''}];}
14
+ return [{...s,notes:s.participantNotes||''}];
15
+ }).map(replace);spec.audience=audience;return spec;
16
+ }
17
+
18
+ export function manualPollHTML(s,head){
19
+ if(!s.id||!s.options?.length)throw new Error('Votação manual precisa de id e opções.');
20
+ const d=s.decision||{},votes=s.options.map((_,i)=>Number(d.votes?.[i])||0);
21
+ return `<div class="L-meeting f-body" data-meeting="${esc(s.id)}">${head({...s,title:s.question||s.title})}<p class="meeting-context">${esc(s.context||'Registre os votos da reunião. A maioria apurada é uma proposta até a aprovação explícita.')}</p><div class="meeting-options">${s.options.map((label,i)=>`<label class="meeting-option"><span>${esc(label)}</span><input aria-label="Votos: ${esc(label)}" type="number" min="0" max="100000" step="1" data-vote="${i}" value="${votes[i]}"><span class="meeting-bar"><i></i></span></label>`).join('')}</div><div class="meeting-actions"><label>Abstenções <input type="number" min="0" max="100000" step="1" data-abstain value="${Number(d.abstentions)||0}"></label><button data-meeting-approve>Aprovar resultado</button><button data-meeting-reopen>Reabrir decisão</button><output aria-live="polite" data-meeting-status></output></div><script type="application/json" class="meeting-state">${JSON.stringify(d).replace(/</g,'\\u003c')}</script></div>`;
22
+ }
23
+
24
+ export function meetingPreview(html,spec,audience){
25
+ const link=spec.slides.some(s=>s.manual)&&audience!=='participants'?'<details style="position:fixed;right:20px;top:10px;z-index:9999;background:#182234;color:white;padding:10px 16px;border-radius:10px;font:14px system-ui"><summary style="cursor:pointer">Versão para participantes</summary><nav style="display:grid;gap:12px;padding-top:14px"><a style="color:white" href="/preview?audience=participants" data-participant-preview>Ver prévia</a><a style="color:white" href="/api/export/html?audience=participants">Baixar HTML interativo</a><a style="color:white" href="/api/export/pdf?audience=participants">Baixar PDF</a><a style="color:white" href="/api/export/pptx?audience=participants">Baixar PowerPoint</a></nav></details>':'';
26
+ return html.replace('</body>',link+'</body>');
27
+ }
28
+
29
+ export async function meetingRoutes({req,res,pathname,W,readJSON,persist}){
30
+ if(pathname!=='/api/meeting')return false;
31
+ const send=(status,data)=>{res.writeHead(status,{'Content-Type':'application/json'});res.end(JSON.stringify(data));};
32
+ if(req.method!=='POST'){send(405,{error:'Use POST.'});return true;}
33
+ const b=await readJSON(req),s=W.spec?.slides.find(s=>s.layout==='poll'&&s.manual&&s.id===b.id);
34
+ if(!W.file){send(409,{error:'Salve uma cópia na biblioteca antes de registrar decisões.'});return true;}
35
+ if(!s){send(404,{error:'Votação não encontrada.'});return true;}
36
+ const valid=n=>Number.isSafeInteger(n)&&n>=0&&n<=100000;
37
+ if(!Array.isArray(b.votes)||b.votes.length!==s.options.length||!b.votes.every(valid)||!valid(b.abstentions)){send(400,{error:'Informe contagens inteiras entre zero e 100000.'});return true;}
38
+ const max=Math.max(...b.votes),winners=b.votes.flatMap((v,i)=>v===max?[i]:[]);
39
+ if(b.approved&&(max===0||winners.length!==1)){send(400,{error:'Sem votos ou empate: a decisão permanece aberta.'});return true;}
40
+ const before=s.decision;s.decision={votes:b.votes,abstentions:b.abstentions,approved:!!b.approved,selected:b.approved?winners[0]:null};
41
+ try{persist(W);send(200,{ok:true,decision:s.decision,...(s.decision.approved&&Number.isFinite(s.values?.[s.decision.selected])?{value:s.values[s.decision.selected]}:{})});}catch(e){s.decision=before;send(500,{error:e.message});}return true;
42
+ }
package/src/motion.js ADDED
@@ -0,0 +1,56 @@
1
+ import {esc} from './markup.js';
2
+ import {playgroundDocument} from './playground.js';
3
+ import {drawHologram} from './hologram.js';
4
+ const number=(x,f,min,max)=>Math.min(max,Math.max(min,Number.isFinite(Number(x))?Number(x):f));
5
+ const color=(x,f)=>/^#?[0-9a-f]{6}$/i.test(x||'')?'#'+String(x).replace('#',''):f;
6
+ export function motionOptions(value,colors={}){
7
+ const o=typeof value==='string'?{type:value}:value||{};
8
+ if(!['wireframe','hologram','trace','requests','flow','chat','terminal'].includes(o.type))throw new Error('motion.type: wireframe, hologram, trace, requests, flow, chat ou terminal.');
9
+ const aliases={fg:'ink',bg:'paper',hi:'accent',em:'alert'};
10
+ const resolved=(v,f)=>color(colors[aliases[v]||v]||v,f);
11
+ const trace=(Array.isArray(o.path)&&o.path.length>=2?o.path:[[.05,.5],[.95,.5]]).slice(0,64).map(p=>[number(Array.isArray(p)?p[0]:p?.x,0,0,1),number(Array.isArray(p)?p[1]:p?.y,0,0,1)]);
12
+ const transparent=o.background==='transparent';
13
+ const layers=(Array.isArray(o.layers)&&o.layers.length?o.layers:[{shape:o.shape,x:.5,y:.5,scale:.24}]).slice(0,6).map(l=>({shape:['sphere','cube','plane','ring'].includes(l.shape)?l.shape:'sphere',x:number(l.x,.5,-.5,1.5),y:number(l.y,.5,-.5,1.5),scale:number(l.scale,.24,.03,1),speed:number(l.speed,1,.1,4),rotation:number(l.rotation,0,-10,10)}));
14
+ return {type:o.type,path:trace,closed:o.closed===true,pulses:Math.round(number(o.pulses,2,1,8)),lineWidth:number(o.lineWidth,1.2,.5,6),layers,opacity:number(o.opacity,.7,.05,1),lines:Array.isArray(o.lines)?o.lines.map(String):String(o.code||'').split('\n'),fontSize:number(o.fontSize,16,10,40),shape:o.shape==='cube'?'cube':'sphere',speed:number(o.speed,1,.1,8),recognition:number(o.recognition,1,0,10),thinking:number(o.thinking,.8,0,10),read:number(o.read,2,0,15),typing:number(o.typing,.035,.01,.2),caption:String(o.caption||''),user:String(o.user||''),assistant:String(o.assistant||''),background:transparent?'transparent':resolved(o.background,'#111e30'),foreground:resolved(o.foreground,transparent?color(colors.ink,'#182642'):'#b5c9db'),accent:resolved(o.accent,color(colors.alert,'#cc092f')),secondary:resolved(o.secondary,transparent?color(colors.accent,'#3769e8'):'#aa9ccf'),manual:o.start!=='auto',finish:['next','video','hold'].includes(o.finish)?o.finish:'hold'};
15
+ }
16
+ function drawTrace(ctx,o,t,w,h){
17
+ const points=o.path.map(p=>[p[0]*w,p[1]*h]);if(o.closed)points.push(points[0]);
18
+ const lengths=points.slice(1).map((p,i)=>Math.hypot(p[0]-points[i][0],p[1]-points[i][1])),total=lengths.reduce((a,b)=>a+b,0);if(!total)return;
19
+ const at=distance=>{let d=((distance%total)+total)%total;for(let i=0;i<lengths.length;i++){if(d<=lengths[i]&&lengths[i]){const f=d/lengths[i];return[points[i][0]+(points[i+1][0]-points[i][0])*f,points[i][1]+(points[i+1][1]-points[i][1])*f];}d-=lengths[i];}return points[0];};
20
+ ctx.save();ctx.lineWidth=o.lineWidth;ctx.globalAlpha=o.opacity*.22;ctx.strokeStyle=o.foreground;ctx.beginPath();points.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p));ctx.stroke();
21
+ for(let k=0;k<o.pulses;k++){const head=(t*.1+k/o.pulses)*total;for(let j=12;j>=0;j--){const p=at(head-j*Math.min(4,total/150));ctx.globalAlpha=o.opacity*(1-j/13);ctx.fillStyle=o.accent;ctx.beginPath();ctx.arc(...p,j===0?2.4:1.2,0,Math.PI*2);ctx.fill();}}
22
+ ctx.restore();
23
+ }
24
+ function scene(o){
25
+ let active=o.type!=='chat',started=false,epoch=performance.now(),serial=0,phase='idle',elapsed=0,previous=0;let reduced=matchMedia('(prefers-reduced-motion:reduce)').matches;const main=document.querySelector('main'),canvas=document.querySelector('canvas'),ctx=canvas.getContext('2d');
26
+ const message=(kind)=>parent.postMessage({sagaMotion:kind},'*');
27
+ const chat=document.querySelector('.chat'),input=document.querySelector('.input'),thread=document.querySelector('.thread'),status=document.querySelector('.status');
28
+ function reset(){started=false;phase='idle';elapsed=0;chat.hidden=true;input.textContent='';thread.replaceChildren();status.textContent='';}
29
+ window.addEventListener('message',e=>{if(e.source!==parent)return;if(e.data?.sagaMotion==='poster'){reduced=true;if(o.type==='terminal')document.querySelector('.feed').textContent=o.lines.join('\n');}if(e.data?.sagaMotion==='active')active=!!e.data.active;if(e.data?.sagaMotion==='reset')reset();if(e.data?.sagaMotion==='start'){reset();started=true;active=true;phase='recognize';chat.hidden=false;}});
30
+ if(o.type==='chat'&&o.manual){const request=e=>{if(!active)return;if(e.type==='keydown'&&![' ','Enter','ArrowRight','PageDown'].includes(e.key))return;e.preventDefault();message('start-request');};document.addEventListener('click',request);document.addEventListener('keydown',request);}
31
+ if(o.type==='chat'&&!o.manual){started=true;phase='recognize';chat.hidden=false;}
32
+ function bubble(text,kind){const el=document.createElement('div');el.className='bubble '+kind;el.textContent=text;thread.append(el);return el;}
33
+ let terminalElapsed=0;const terminalText=o.lines.join('\n');
34
+ let bot;
35
+ function tickChat(dt){if(!started||phase==='done')return;elapsed+=dt;
36
+ if(phase==='recognize'&&elapsed>=o.recognition){phase='input';elapsed=0;}
37
+ if(phase==='input'){input.textContent=o.user.slice(0,Math.floor(elapsed/o.typing));if(elapsed>=o.user.length*o.typing+.35){bubble(o.user,'user');input.textContent='';status.textContent='Pensando…';phase='thinking';elapsed=0;}}
38
+ else if(phase==='thinking'&&elapsed>=o.thinking){status.textContent='';bot=bubble('','assistant');phase='answer';elapsed=0;}
39
+ else if(phase==='answer'){bot.textContent=o.assistant.slice(0,Math.floor(elapsed/o.typing));if(elapsed>=o.assistant.length*o.typing){phase='read';elapsed=0;}}
40
+ else if(phase==='read'&&elapsed>=o.read){phase='done';message('done');}}
41
+ function requestLine(){serial++;const ops=['GET /silent-cloud','POST /paper-moon','PUT /silver-orbit','PATCH /soft-vector','GET /tiny-comet','DELETE /ghost-frame'];const el=document.createElement('div');el.textContent=String(serial).padStart(6,'0')+' '+ops[(serial*7+Math.floor(serial/9))%ops.length].padEnd(23,' ')+' '+(serial*2654435761>>>0).toString(16)+' '+(17+serial*37%981)+'ms 200 OK';document.querySelector('.feed').append(el);while(document.querySelector('.feed').children.length>14)document.querySelector('.feed').firstChild.remove();document.querySelector('.feed').scrollTop=1e6;}
42
+ if(o.type==='requests'){for(let i=0;i<8;i++)requestLine();setInterval(()=>{if(active&&!document.hidden&&!reduced){requestLine();requestLine();}},Math.max(35,240/o.speed));}
43
+ function draw(ts){requestAnimationFrame(draw);if(!active||document.hidden){previous=ts;return;}const dt=Math.min(.1,(ts-(previous||ts))/1000);previous=ts;if(o.type==='chat'){tickChat(dt);return;}if(o.type==='terminal'){terminalElapsed+=dt*o.speed;const count=reduced?terminalText.length:Math.floor(terminalElapsed/o.typing);document.querySelector('.feed').textContent=terminalText.slice(0,count)+(count<terminalText.length?'_':'');document.querySelector('.feed').scrollTop=1e6;if(count>terminalText.length+3/o.typing)terminalElapsed=0;return;}if(o.type==='requests'||(reduced&&serial))return;serial++;
44
+ const w=main.clientWidth,h=main.clientHeight,dpr=devicePixelRatio||1;if(canvas.width!==Math.round(w*dpr)||canvas.height!==Math.round(h*dpr)){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);}ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);const t=reduced?0:(ts-epoch)/1000*o.speed,r=Math.min(w*.25,h*.32),cy=h*.55;if(o.type==='hologram'){drawHologram(ctx,o,t,w,h);return;}if(o.type==='trace'){drawTrace(ctx,o,t,w,h);return;}
45
+ function p(x,y,z){let a=t*.15+.4,xx=x*Math.cos(a)+z*Math.sin(a),zz=-x*Math.sin(a)+z*Math.cos(a),yy=y*Math.cos(.3)-zz*Math.sin(.3);const f=4/(4+zz);return[w/2+xx*r*f,cy+yy*r*f];}
46
+ function line(v,c){ctx.beginPath();v.forEach((q,i)=>{let a=p(...q);i?ctx.lineTo(...a):ctx.moveTo(...a);});ctx.strokeStyle=c;ctx.lineWidth=.9;ctx.stroke();}
47
+ if(o.type==='flow'){let y=h*.55,x1=w*.12,x2=w*.5,x3=w*.88;ctx.strokeStyle=o.foreground;ctx.lineWidth=1.2;ctx.beginPath();ctx.moveTo(x1,y-18);ctx.bezierCurveTo(w*.35,y-18,w*.35,y,x2,y);ctx.lineTo(x3,y);ctx.moveTo(x1,y+18);ctx.bezierCurveTo(w*.35,y+18,w*.35,y,x2,y);ctx.stroke();for(const [x,yy]of[[x1,y-18],[x1,y+18],[x2,y],[x3,y]]){ctx.fillStyle=x===x3?o.accent:o.background;ctx.fillRect(x-7,yy-7,14,14);ctx.strokeRect(x-7,yy-7,14,14);}for(let j=0;j<2;j++){let n=(t*.15+j*.5)%1;ctx.fillStyle=o.secondary;ctx.beginPath();ctx.arc(x1+n*(x3-x1),y+(j?-18:18)*Math.max(0,1-n*3),2.5,0,Math.PI*2);ctx.fill();}return;}
48
+ if(o.shape==='cube'){const v=[[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]];for(const [a,b]of[[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]])line([v[a],v[b]],o.foreground);}
49
+ else{for(let j=-3;j<=3;j++){let v=[],phi=j*Math.PI/9;for(let n=0;n<=60;n++){let a=n*Math.PI/30;v.push([Math.cos(phi)*Math.cos(a),Math.sin(phi),Math.cos(phi)*Math.sin(a)]);}line(v,o.foreground);}for(let j=0;j<10;j++){let v=[],a=j*Math.PI/5;for(let n=0;n<=60;n++){let phi=-Math.PI/2+n*Math.PI/60;v.push([Math.cos(phi)*Math.cos(a),Math.sin(phi),Math.cos(phi)*Math.sin(a)]);}line(v,o.foreground);}}
50
+ let v=[];for(let n=0;n<=80;n++){let a=n*Math.PI/40;v.push([1.4*Math.cos(a),.3*Math.sin(a),.8*Math.sin(a)]);}line(v,o.secondary);const q=p(Math.cos(t*.15),0,Math.sin(t*.15));ctx.fillStyle=o.accent;ctx.fillRect(q[0]-2,q[1]-2,4,4);
51
+ }
52
+ requestAnimationFrame(draw);
53
+ }
54
+ export function motionHTML(value,colors){const o=motionOptions(value,colors);const css=`html,body{height:100%;background:${o.type==='chat'?'transparent':o.background};color:${o.foreground};overflow:hidden}main{height:100%;position:relative}canvas{position:absolute;inset:0;width:100%;height:100%}.caption{position:absolute;top:12px;left:16px;color:${o.secondary};font:11px monospace;letter-spacing:1.5px}.feed{position:absolute;top:36px;left:16px;right:16px;bottom:12px;overflow:hidden;font:${o.type==='terminal'?o.fontSize:12}px/1.5 monospace;white-space:pre}.chat{position:absolute;inset:0;border:1px solid ${o.secondary};border-radius:16px;padding:18px;background:${o.background};display:flex;flex-direction:column;gap:12px}.chat[hidden]{display:none}.thread{flex:1;overflow:auto;display:flex;flex-direction:column;gap:12px}.bubble{font:18px/1.45 system-ui;max-width:90%;padding:10px 14px;border-radius:10px;white-space:pre-wrap}.user{align-self:flex-end;background:#263a51}.assistant{align-self:flex-start}.status{font:14px system-ui;color:${o.secondary};min-height:18px}.input{border:1px solid #51617b;border-radius:9px;min-height:48px;padding:12px;font:17px system-ui;white-space:pre-wrap}.chat-title{font:12px system-ui;letter-spacing:2px;color:${o.secondary}}`;
55
+ const html=`<main><canvas aria-hidden="true"></canvas><div class="caption">${esc(o.type==='chat'?'':o.caption)}</div><div class="feed" aria-hidden="true"></div><section class="chat" hidden><div class="chat-title">${esc(o.caption||'ASSISTENTE')}</div><div class="thread" aria-live="polite"></div><div class="status"></div><div class="input" aria-label="Mensagem digitada"></div></section></main>`;
56
+ const doc=playgroundDocument({html,css,javascript:`${drawHologram.toString()}\n${drawTrace.toString()}\n(${scene.toString()})(${JSON.stringify(o)});`});return `<iframe data-motion="${o.type}" data-motion-manual="${o.manual}" data-motion-finish="${o.finish}" title="${esc(o.caption||'Detalhe animado')}" sandbox="allow-scripts" style="width:100%;height:100%;border:0" srcdoc="${esc(doc)}"></iframe>`;}
package/src/package.js ADDED
@@ -0,0 +1,116 @@
1
+ // Formato .sagadeck: a apresentação inteira num arquivo só — o YAML + todos os arquivos locais que ele
2
+ // usa (imagens, CSS, widgets). Por dentro é um .zip (como .pptx e .docx):
3
+ //
4
+ // sagadeck.json manifesto: { format: "sagadeck", version, main, title, created, generator }
5
+ // <nome>.yaml a apresentação (sem campos internos da máquina de quem salvou)
6
+ // imagens/, widgets/… os arquivos, nos mesmos caminhos relativos do YAML
7
+ // assets/ arquivos que estavam FORA da pasta do deck (o YAML do pacote aponta para cá)
8
+ // FALTANDO.txt o que o deck usa mas não existia ao salvar
9
+ //
10
+ // Também abre um .zip comum com um .yaml dentro (versão sem manifesto).
11
+ //
12
+ // deckAssets(spec, baseDir) -> [{ ref, rel, abs, exists }] o que o deck usa do disco
13
+ // packDeck(spec, { baseDir, name }) -> { zip: Buffer, files, missing }
14
+ // unpackDeck(buffer, destDir) -> { file, dir, manifest } (caminho do .yaml extraído)
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import JSZip from "jszip";
18
+ import YAML from "yaml";
19
+
20
+ export const EXTENSION = ".sagadeck";
21
+ export const MIME = "application/vnd.sagadeck+zip";
22
+ export const FORMAT_VERSION = 1; // sobe quando o formato mudar de um jeito que versões antigas não entendam
23
+
24
+ const isLocal = (v) => typeof v === "string" && v.trim() && !/^(https?:|data:|\/\/)/i.test(v.trim());
25
+ const posix = (p) => p.split(path.sep).join("/");
26
+
27
+ // percorre o deck achando referências a arquivos: css, widgets e qualquer `image`
28
+ function refsOf(spec) {
29
+ const out = [];
30
+ const add = (v, where) => { if (isLocal(v)) out.push({ ref: v.trim(), where }); };
31
+ [].concat(spec.css || []).forEach((c) => add(c, "css"));
32
+ [].concat(spec.widgets || []).forEach((w) => add(w, "widget"));
33
+ const walk = (v) => {
34
+ if (Array.isArray(v)) return v.forEach(walk);
35
+ if (!v || typeof v !== "object") return;
36
+ for (const [k, x] of Object.entries(v)) {
37
+ if (k === "image") add(x, "image");
38
+ else if (typeof x === "object") walk(x);
39
+ }
40
+ };
41
+ walk(spec.slides || []);
42
+ return out;
43
+ }
44
+
45
+ export function deckAssets(spec, baseDir) {
46
+ const seen = new Map();
47
+ for (const { ref, where } of refsOf(spec)) {
48
+ if (seen.has(ref)) continue;
49
+ const abs = path.resolve(baseDir, ref);
50
+ const inside = !path.relative(baseDir, abs).startsWith("..") && !path.isAbsolute(path.relative(baseDir, abs));
51
+ // dentro da pasta do deck: mesmo caminho relativo; fora: assets/<nome> (sem colidir)
52
+ let rel = inside ? posix(path.relative(baseDir, abs)) : `assets/${path.basename(abs)}`;
53
+ if (!inside) for (let n = 2; [...seen.values()].some((a) => a.rel === rel); n++) rel = `assets/${n}-${path.basename(abs)}`;
54
+ seen.set(ref, { ref, where, rel, abs, exists: fs.existsSync(abs) && fs.statSync(abs).isFile() });
55
+ }
56
+ return [...seen.values()];
57
+ }
58
+
59
+ // YAML do pacote: sem campos internos (_dir, _file…) e com os caminhos que mudaram (arquivos de fora)
60
+ function packagedSpec(spec, assets) {
61
+ const moved = new Map(assets.filter((a) => a.ref !== a.rel).map((a) => [a.ref, a.rel]));
62
+ const clean = (v, key) => {
63
+ if (Array.isArray(v)) return v.map((x) => clean(x, key));
64
+ if (typeof v === "string") return (key === "image" || key === "css" || key === "widgets") && moved.has(v.trim()) ? moved.get(v.trim()) : v;
65
+ if (!v || typeof v !== "object") return v;
66
+ return Object.fromEntries(Object.entries(v).filter(([k]) => !k.startsWith("_")).map(([k, x]) => [k, clean(x, k)]));
67
+ };
68
+ return clean(spec);
69
+ }
70
+
71
+ export async function packDeck(spec, { baseDir, name = "apresentacao", generator = "sagadeck" } = {}) {
72
+ const dir = baseDir || spec._dir || process.cwd();
73
+ const assets = deckAssets(spec, dir);
74
+ const zip = new JSZip();
75
+ const root = zip;
76
+ const main = `${name}.yaml`;
77
+ root.file("sagadeck.json", JSON.stringify({ format: "sagadeck", version: FORMAT_VERSION, main, title: spec.title || name,
78
+ created: new Date().toISOString(), generator }, null, 2));
79
+ root.file(main, YAML.stringify(packagedSpec(spec, assets), { indent: 2, lineWidth: 0 }));
80
+ const files = [], missing = [];
81
+ for (const a of assets) {
82
+ if (a.exists) { root.file(a.rel, fs.readFileSync(a.abs)); files.push(a.rel); }
83
+ else missing.push(a.ref);
84
+ }
85
+ if (missing.length) {
86
+ root.file("FALTANDO.txt", `Estes arquivos são usados pela apresentação, mas não foram encontrados em ${dir}:\n\n` +
87
+ missing.map((m) => `- ${m}`).join("\n") + "\n\nColoque-os nesta pasta (mesmo caminho) para a apresentação ficar completa.\n");
88
+ }
89
+ return { zip: await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }), files, missing };
90
+ }
91
+
92
+ // Extrai um pacote numa pasta nova (nunca sobrescreve) e devolve o .yaml principal.
93
+ export async function unpackDeck(buffer, destDir) {
94
+ const zip = await JSZip.loadAsync(buffer);
95
+ const entries = Object.values(zip.files).filter((f) => !f.dir);
96
+ for (const f of entries) {
97
+ const norm = path.posix.normalize(f.name.replace(/\\/g, "/"));
98
+ if (norm.startsWith("../") || norm.startsWith("/") || /^[a-z]:/i.test(norm)) throw new Error(`Pacote inválido: caminho "${f.name}" sai da pasta`);
99
+ }
100
+ let manifest = null;
101
+ const mf = zip.file("sagadeck.json");
102
+ if (mf) {
103
+ try { manifest = JSON.parse(await mf.async("string")); } catch { throw new Error("Arquivo .sagadeck com manifesto (sagadeck.json) inválido."); }
104
+ if (manifest.version > FORMAT_VERSION) throw new Error(`Este arquivo foi salvo por uma versão mais nova do sagadeck (formato ${manifest.version}; esta entende até ${FORMAT_VERSION}). Atualize o sagadeck.`);
105
+ }
106
+ const yamls = manifest?.main && zip.file(manifest.main) ? [manifest.main]
107
+ : entries.map((f) => f.name).filter((n) => /\.ya?ml$/i.test(n)).sort((a, b) => a.split("/").length - b.split("/").length);
108
+ if (!yamls.length) throw new Error("O arquivo não tem nenhuma apresentação (.yaml) dentro.");
109
+ fs.mkdirSync(destDir, { recursive: true });
110
+ for (const f of entries) {
111
+ const target = path.join(destDir, ...path.posix.normalize(f.name).split("/"));
112
+ fs.mkdirSync(path.dirname(target), { recursive: true });
113
+ fs.writeFileSync(target, await f.async("nodebuffer"));
114
+ }
115
+ return { file: path.join(destDir, ...yamls[0].split("/")), dir: destDir, manifest };
116
+ }
@@ -0,0 +1,19 @@
1
+ import { esc } from './markup.js';
2
+
3
+ // Conteúdo executável tem uma origem opaca e nenhuma permissão de rede ou do pai.
4
+ export function playgroundDocument(s) {
5
+ const policy = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; media-src data: blob:; connect-src 'none'";
6
+ const bridge = `const report=(kind,text)=>parent.postMessage({sagaPlayground:true,kind,text:String(text).slice(0,4000)},'*');window.addEventListener('error',e=>report('error',e.message));window.addEventListener('unhandledrejection',e=>report('error',e.reason));console.log=(...x)=>report('log',x.join(' '));window.sagaActive=true;window.addEventListener('message',e=>{if(e.source===parent&&typeof e.data?.sagaActive==='boolean')window.sagaActive=e.data.sagaActive});const interval=window.setInterval;window.setInterval=(fn,ms,...args)=>interval(()=>{if(window.sagaActive)fn(...args)},ms);`;
7
+ const script = String(s.javascript || '').replace(/<\/script/gi, '<\\/script');
8
+ return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${esc(policy)}"><style>html,body{margin:0;height:100%;font-family:system-ui;background:#f6f7fa;color:#202432}*{box-sizing:border-box}${s.css || ''}</style></head><body>${s.html || ''}<script>${bridge}\n${script}</script></body></html>`;
9
+ }
10
+ export function playgroundHTML(s, ctx, head) {
11
+ return `<div class="L-playground f-body" data-playground>${head(s)}<div class="pg-toolbar"><button data-pg-reset>Reiniciar experiência</button><output data-pg-status aria-live="polite"></output></div><iframe class="pg-frame" title="${esc(s.title || 'Experiência interativa')}" sandbox="allow-scripts" srcdoc="${esc(playgroundDocument(s))}"></iframe>${s.explanation ? `<p class="gl-explanation">${esc(s.explanation)}</p>` : ''}</div>`;
12
+ }
13
+ export const playgroundExample = {
14
+ layout: 'playground', title: 'Uma fila. Dois ritmos.',
15
+ html: '<main><h2>Chegadas e atendimento</h2><label>Capacidade <input id="capacity" type="range" min="1" max="8" value="3"></label><button id="add">Chegaram mais cinco pedidos</button><p id="stats"></p><div id="queue"></div></main>',
16
+ css: 'main{padding:32px}#queue{display:flex;flex-wrap:wrap;gap:12px;margin-top:24px}.job{width:48px;height:48px;background:#7756dc;border-radius:12px;transition:opacity .5s,transform .5s}button,input{margin:12px}button{padding:12px}',
17
+ javascript: `let next=0,done=0;const q=document.querySelector('#queue');function add(){for(let i=0;i<5;i++){const e=document.createElement('div');e.className='job';e.title='Pedido '+(++next);q.append(e)}paint()}function paint(){document.querySelector('#stats').textContent=q.children.length+' na fila; '+done+' atendidos'}document.querySelector('#add').onclick=add;setInterval(()=>{const n=Number(document.querySelector('#capacity').value);for(const e of [...q.children].slice(0,n)){e.style.opacity=0;e.style.transform='translateY(-20px)';setTimeout(()=>{e.remove();done++;paint()},500)}},1600);add();`,
18
+ explanation: 'Aumente as chegadas e altere a capacidade. Em que condições a fila consegue diminuir?',
19
+ };