sagadeck 1.7.1 → 1.9.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 (105) hide show
  1. package/README.md +55 -4
  2. package/SKILL.md +10 -3
  3. package/bin/sagadeck.js +23 -0
  4. package/docs/ANDAMENTO.md +483 -1
  5. package/docs/PLANO-MELHORIAS.md +180 -0
  6. package/docs/REFERENCIA.md +184 -105
  7. package/package.json +34 -2
  8. package/src/ai/commands.js +2 -2
  9. package/src/ai/deck-ai.js +14 -5
  10. package/src/ai/ia-check.js +127 -0
  11. package/src/ai/ia-config.js +2 -2
  12. package/src/ai/llm.js +5 -1
  13. package/src/ai/map-analysis-run.js +119 -0
  14. package/src/ai/map-data.js +221 -0
  15. package/src/ai/quality.js +1 -1
  16. package/src/ai/video-generation.js +70 -25
  17. package/src/build.js +51 -15
  18. package/src/elements.js +39 -1
  19. package/src/export/browser.js +23 -1
  20. package/src/export/pptx.js +9 -3
  21. package/src/export/shots.js +10 -3
  22. package/src/fiscal/normalize.js +19 -0
  23. package/src/fs-retry.js +37 -2
  24. package/src/import/office.js +5 -4
  25. package/src/layouts.js +114 -99
  26. package/src/lessons.js +1 -1
  27. package/src/library.js +7 -7
  28. package/src/map-analysis.js +109 -0
  29. package/src/map-config.js +89 -0
  30. package/src/map-routing.js +82 -0
  31. package/src/map-services.js +60 -0
  32. package/src/map.js +274 -0
  33. package/src/mcp/server.js +17 -0
  34. package/src/motion.js +20 -6
  35. package/src/purpose.js +1 -1
  36. package/src/runtime/base.css +105 -329
  37. package/src/runtime/fit.js +3 -2
  38. package/src/runtime/fonts/amatic-sc.css +3 -0
  39. package/src/runtime/fonts/bebas-neue.css +2 -0
  40. package/src/runtime/fonts/cabin-sketch.css +2 -0
  41. package/src/runtime/fonts/fraunces.css +3 -0
  42. package/src/runtime/fonts/gelasio.css +3 -0
  43. package/src/runtime/fonts/ibm-plex-mono.css +4 -0
  44. package/src/runtime/fonts/index.json +10 -1
  45. package/src/runtime/fonts/itim.css +2 -0
  46. package/src/runtime/fonts/montserrat.css +2 -0
  47. package/src/runtime/fonts/space-grotesk.css +2 -0
  48. package/src/runtime/map-background.js +91 -0
  49. package/src/runtime/map.js +266 -0
  50. package/src/runtime/runtime.js +81 -100
  51. package/src/runtime/skins/caderno.css +33 -0
  52. package/src/runtime/skins/cartaz.css +27 -0
  53. package/src/runtime/skins/infografico.css +29 -0
  54. package/src/runtime/skins/lousa.css +22 -0
  55. package/src/runtime/skins/musgo.css +42 -0
  56. package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
  57. package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
  58. package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
  59. package/src/runtime/vendor/geo-contornos.json +1 -0
  60. package/src/runtime/vendor/leaflet.css +661 -0
  61. package/src/runtime/vendor/leaflet.js +6 -0
  62. package/src/runtime/vendor/maplibre-gl.css +1 -0
  63. package/src/runtime/vendor/maplibre-gl.js +825 -0
  64. package/src/runtime/vendor/maplibre-leaflet.js +234 -0
  65. package/src/runtime/vendor/maplibre-worker.js +10 -0
  66. package/src/studio/ai-chat-route.js +20 -0
  67. package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
  68. package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
  69. package/src/studio/command-approval.js +2 -3
  70. package/src/studio/demo-decks.js +6 -2
  71. package/src/studio/export-jobs.js +86 -0
  72. package/src/studio/exporting.js +36 -18
  73. package/src/studio/feature-demos.js +144 -0
  74. package/src/studio/geo-routes.js +138 -0
  75. package/src/studio/ia-routes.js +9 -0
  76. package/src/studio/layout-samples.js +78 -16
  77. package/src/studio/library-routes.js +0 -9
  78. package/src/studio/project.js +2 -1
  79. package/src/studio/public/ai-settings.js +6 -3
  80. package/src/studio/public/app.js +46 -52
  81. package/src/studio/public/explorer.js +23 -8
  82. package/src/studio/public/export-status.js +154 -0
  83. package/src/studio/public/index.html +17 -10
  84. package/src/studio/public/library.css +12 -3
  85. package/src/studio/public/library.html +3 -0
  86. package/src/studio/public/library.js +39 -12
  87. package/src/studio/public/map-data.js +158 -0
  88. package/src/studio/public/map-editor.js +676 -0
  89. package/src/studio/public/map-fields.js +21 -0
  90. package/src/studio/public/map-settings.js +86 -0
  91. package/src/studio/public/slide-form.js +19 -16
  92. package/src/studio/public/style.css +58 -0
  93. package/src/studio/public/ui-icons.js +1 -1
  94. package/src/studio/public/viewers.js +2 -1
  95. package/src/studio/public/visual-editor.js +1 -1
  96. package/src/studio/server.js +34 -9
  97. package/src/studio/snapshot.js +8 -4
  98. package/src/studio/template-collections.js +1 -1
  99. package/src/studio/tour.js +158 -0
  100. package/src/themes.js +154 -0
  101. package/templates/cenario-e-ciencia.yaml +1 -21
  102. package/templates/ensaio-api.yaml +3 -3
  103. package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
  104. package/templates/cenario/imagens/cidade.svg +0 -30
  105. package/templates/cenario/imagens/pessoa.svg +0 -15
@@ -6,6 +6,7 @@ import {chatErrorResult,installErrorResponses} from './errors.js';
6
6
  import { slideCopyRoutes } from "./slide-copy-routes.js";
7
7
  import { publicScript } from "./public-files.js";
8
8
  import { iaRoutes } from "./ia-routes.js";
9
+ import { geoRoutes } from "./geo-routes.js";
9
10
  import { codeWatch } from "./code-version.js";
10
11
  import {meetingRoutes,meetingPreview} from '../meeting.js';
11
12
  import { codeRoutes } from "./code-routes.js";
@@ -45,6 +46,7 @@ import { llmAvailable, llmConfig, llmConfigured } from "../ai/llm.js";
45
46
  import { editDeck, textToSlide, toYaml, materializeImages } from "../ai/deck-ai.js";
46
47
  import { transformDeck, jobStatus, sourceHash } from "../ai/transform.js";
47
48
  import { sendExport, sendHtml, lightVariant } from "./exporting.js";
49
+ import { createExportJobs } from './export-jobs.js';
48
50
  import { styleRoutes, styleAction } from "./style-routes.js";
49
51
  import { shareRoutes } from "./share-routes.js";
50
52
  import { apiRoutes } from "./api-routes.js";
@@ -91,6 +93,7 @@ export function createStudioServer(deckPath = null, opts = {}) {
91
93
  const STARTED = new Date().toISOString();
92
94
  const BUILD = buildInfo(); // carimbo calculado uma vez na subida (git pode demorar)
93
95
  const workspaces = new Map();
96
+ const exports = createExportJobs();
94
97
  const slideClipboard = new Map();
95
98
  function sampleSpec() {
96
99
  const samplePath = TEMPLATE_DIRS.map((d) => path.join(d, "exemplo.yaml")).find((f) => fs.existsSync(f)) || "";
@@ -258,13 +261,6 @@ export function createStudioServer(deckPath = null, opts = {}) {
258
261
  key = String(key || "");
259
262
  if (key.startsWith("model-")) { const kind = key.slice(6); return { spec: demoDeck(kind), dir: MODEL_ASSETS_DIR, copy: (L, id) => copyModelAssets(L, id, kind) }; }
260
263
  if (key.startsWith("exp-")) return { spec: createExperienceDeck(key.slice(4)), dir: null, copy: () => {} };
261
- if (key === "example-cenario") {
262
- const dir0 = TEMPLATE_DIRS.map((d) => path.join(d, "cenario")).find((d) => fs.existsSync(d));
263
- if (!dir0) throw new Error("o exemplo não veio no pacote (templates/cenario)");
264
- const tpl = fs.readdirSync(dir0).find((f) => f.endsWith(".yaml"));
265
- return { spec: YAML.parse(fs.readFileSync(path.join(dir0, tpl), "utf8")), dir: dir0,
266
- copy: (L, id) => fs.cpSync(path.join(dir0, "imagens"), path.join(path.dirname(L.resolveId(id)), "imagens"), { recursive: true, force: false }) };
267
- }
268
264
  if (key === "example-api") {
269
265
  const tpl = templateFile("ensaio-api.yaml");
270
266
  if (!tpl) throw new Error("o exemplo não veio no pacote (templates/ensaio-api.yaml)");
@@ -493,6 +489,13 @@ export function createStudioServer(deckPath = null, opts = {}) {
493
489
  res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/diagram.js" ? "diagram.js" : "vendor/mermaid.min.js")));
494
490
  return;
495
491
  }
492
+ // mapa ao vivo: bibliotecas locais, carregadas no primeiro mapa que as usa
493
+ if (["/map.js", "/map-background.js", "/leaflet.js", "/leaflet.css", "/geo-contornos.json", "/maplibre-gl.js", "/maplibre-gl.css", "/maplibre-worker.js", "/maplibre-leaflet.js"].includes(pathname)) {
494
+ const type = pathname.endsWith(".css") ? "text/css" : pathname.endsWith(".json") ? "application/json" : "application/javascript";
495
+ res.writeHead(200, { "Content-Type": `${type}; charset=utf-8` }); // sem cache: depois de atualizar, o mapa novo vale na hora
496
+ res.end(fs.readFileSync(path.join(RUNTIME_DIR, ["/map.js", "/map-background.js"].includes(pathname) ? pathname.slice(1) : `vendor${pathname}`)));
497
+ return;
498
+ }
496
499
  if (["/science.js", "/formula.js", "/calc.js", "/plotly.min.js"].includes(pathname)) {
497
500
  res.writeHead(200, {"Content-Type":"application/javascript; charset=utf-8"});
498
501
  res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/plotly.min.js" ? "vendor/plotly.min.js" : pathname.slice(1))));
@@ -895,6 +898,8 @@ export function createStudioServer(deckPath = null, opts = {}) {
895
898
 
896
899
  // Configurar IA (provedor, chave, modelos desta máquina): src/studio/ia-routes.js
897
900
  if (pathname.startsWith("/api/ia") && await iaRoutes({ req, res, pathname, opts, readJSON })) return;
901
+ // Configurar mapa, estado dos serviços e coordenadas pelo endereço: src/studio/geo-routes.js
902
+ if (pathname.startsWith("/api/mapa") && await geoRoutes({ req, res, pathname, opts, readJSON, W, isBundledTemplate })) return;
898
903
 
899
904
  if (pathname === "/api/ai/status" && req.method === "GET") {
900
905
  const cfg = llmConfig();
@@ -960,7 +965,9 @@ export function createStudioServer(deckPath = null, opts = {}) {
960
965
 
961
966
  // Projeto (a pasta da apresentação, como no VS Code): árvore, arquivos, planilhas e sugestões de gráfico
962
967
  if (pathname.startsWith("/api/project/")) {
963
- const done = (obj = {}, code = 200) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(code === 200 ? { ok: true, ...obj } : obj)); };
968
+ // modelo em prévia: gravar um arquivo do projeto é uma mudança, então a cópia nasce aqui (a tela é avisada)
969
+ const made = !W.file && W.preview && req.method === "POST" ? materializePreview(W) : null;
970
+ const done = (obj = {}, code = 200) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(code === 200 ? { ok: true, ...obj, ...(made ? { materialized: made } : {}) } : obj)); };
964
971
  try {
965
972
  const P = W.file && !isBundledTemplate(W.file) ? Project.projectOf(W.file) : null;
966
973
  if (!P) return done({ error: W.preview ? "Modelo em prévia: a pasta do projeto nasce na primeira mudança." : "Esta apresentação não tem uma pasta só dela (abra pela biblioteca)." }, 409);
@@ -1199,6 +1206,23 @@ Responda só com JSON: {"colunas": [{"nome": "…", "tipo": "tempo|categoria|num
1199
1206
  // slide API: ambientes, variáveis, envio, tempo real, gravação (src/studio/api-routes.js)
1200
1207
  if (pathname.startsWith("/api/http/")) return apiRoutes({ req, res, pathname, url, W, apiEnv, rtSessions, apiBlocked, ensureEnsaio, readJSON, isBundledTemplate });
1201
1208
 
1209
+ if (pathname === '/api/export/jobs' || pathname.startsWith('/api/export/jobs/')) {
1210
+ const owner = W.user || '';
1211
+ const json = (code, body) => { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(body)); };
1212
+ if (pathname === '/api/export/jobs' && req.method === 'GET') return json(200, { jobs: exports.list(owner) });
1213
+ if (pathname === '/api/export/jobs' && req.method === 'POST') {
1214
+ try {
1215
+ const b = await readJSON(req);
1216
+ const file = b.id ? W.library.resolveId(b.id) : W.file;
1217
+ const spec = b.id ? loadSpec(file) : withBase(W, W.spec);
1218
+ const name = file && !isBundledTemplate(file) ? path.basename(file).replace(/\.ya?ml$/i, '') : slugify(spec.title);
1219
+ return json(202, { job: exports.create(owner, b.kind, spec, name, { notes: b.notes !== false, audience: b.audience || undefined, macFonts: b.macFonts === true }) });
1220
+ } catch (e) { return json(400, { error: e.message }); }
1221
+ }
1222
+ const download = pathname.match(/^\/api\/export\/jobs\/([a-f0-9-]+)\/download$/);
1223
+ if (download && req.method === 'GET') return exports.download(owner, download[1], res);
1224
+ return json(405, { error: 'Método ou endereço de exportação não suportado.' });
1225
+ }
1202
1226
  if (pathname.startsWith("/api/library")) return libraryRoutes({ req, res, pathname, url, W, opts, readJSON, readBody, slideSnapshots, sendExport, galleryKey, galleryEntry, materializePreview, copyModelAssets, generateIntoLibrary, llmAvailable, llmConfig, respond, templateFile, TEMPLATE_DIRS });
1203
1227
 
1204
1228
  // Baixar o deck aberto: .sagadeck (YAML + imagens, CSS, widgets), PowerPoint, PDF ou roteiro
@@ -1288,6 +1312,7 @@ Responda só com JSON: {"colunas": [{"nome": "…", "tipo": "tempo|categoria|num
1288
1312
  }
1289
1313
  });
1290
1314
 
1291
- server.on("close", () => { ensaio?.then((mock) => mock?.close()); });
1315
+ server.closeExports = () => exports.close();
1316
+ server.on("close", () => { exports.close(); ensaio?.then((mock) => mock?.close()); });
1292
1317
  return server;
1293
1318
  }
@@ -13,12 +13,13 @@ let browserPromise = null;
13
13
 
14
14
  async function browser() {
15
15
  if (!browserPromise) {
16
- browserPromise = (async () => {
16
+ const pending = (async () => {
17
17
  const { chromium } = await import("playwright-core");
18
18
  const b = await chromium.launch({ executablePath: findBrowser() });
19
- b.on("disconnected", () => { browserPromise = null; });
19
+ b.on("disconnected", () => { if (browserPromise === pending) browserPromise = null; });
20
20
  return b;
21
- })().catch((e) => { browserPromise = null; throw e; });
21
+ })().catch((e) => { if (browserPromise === pending) browserPromise = null; throw e; });
22
+ browserPromise = pending;
22
23
  }
23
24
  return browserPromise;
24
25
  }
@@ -158,6 +159,9 @@ export async function layoutCheck(spec) {
158
159
  }
159
160
 
160
161
  export async function closeSnapshots() {
161
- if (browserPromise) (await browserPromise).close().catch(() => {});
162
+ // Desliga a referência antes de esperar: outra captura pode começar enquanto o Chrome fecha.
163
+ // O disconnected antigo não pode apagar a referência desse navegador novo.
164
+ const pending = browserPromise;
162
165
  browserPromise = null;
166
+ if (pending) await (await pending).close().catch(() => {});
163
167
  }
@@ -24,7 +24,7 @@ const label = (text, x = 120, y = 90, color = 'ink') => txt(text, x, y, 1400, 24
24
24
  const heading = (text, x, y, w, size = 100, extra = {}) => txt(text, x, y, w, size, { as: 'title', ...extra });
25
25
  const item = (title, text, value) => ({ title, text, ...(value ? { value } : {}) });
26
26
  const priorities = [item('Uma pergunta melhor', 'Entenda o que precisa mudar antes de escolher a solução.', '01'), item('Um teste pequeno', 'Aprenda com uma experiência simples e um resultado observável.', '02'), item('Uma decisão clara', 'Combine o próximo passo, o responsável e o momento de revisar.', '03')];
27
- const agenda = ['O contexto', 'A oportunidade', 'O experimento', 'O próximo passo'];
27
+ const agenda = ['O contexto', 'A oportunidade', 'A proposta', 'O próximo passo'];
28
28
  const palette = (paper, ink, accent, alert, family) => Object.fromEntries(Object.entries({ paper, ink, accent, alert, family }).map(([key, value]) => [key, Array.isArray(value) ? value.map(c => c.replace('#', '')) : value.replace('#', '')]));
29
29
 
30
30
  // Estética de cada coleção para a geração com IA (modal "Deck com IA": estilo → tema + direção criativa).
@@ -0,0 +1,158 @@
1
+ // "Conheça o SagaDeck": a apresentação que todo usuário acha na biblioteca, com tudo o que dá para fazer. Monta-se
2
+ // sozinha dos exemplos de layout (layout-samples.js): layout novo entra aqui sem ninguém lembrar (engine.test.js
3
+ // confere). Cada slide diz o nome do recurso e a frase para pedir à IA. Abre em prévia, como os modelos da vitrine.
4
+ import { LAYOUT_INFO, LAYOUT_SAMPLES, LAYOUT_ASK } from "./layout-samples.js";
5
+ import { circle, lineBuffer } from "../map-analysis.js";
6
+
7
+ export const TOUR_TITLE = "Conheça o SagaDeck";
8
+
9
+ // os grupos na ordem em que fazem sentido para quem está conhecendo; layout que não estiver em nenhum entra em "Mais"
10
+ const GROUPS = [
11
+ ["Abrir e fechar", "capa, seções, agenda e encerramento", ["cover", "section", "agenda", "end", "references"]],
12
+ ["Uma ideia por vez", "frases, números e palavras que ocupam o palco", ["statement", "headline", "quote", "definition", "number"]],
13
+ ["Organizar o conteúdo", "listas, cartões, etapas, comparações e linhas do tempo", ["list", "cards", "mosaic", "ribbon", "bento", "split", "compare", "matrix", "steps", "timeline", "calendar", "hub", "infographic", "pyramid", "funnel"]],
14
+ ["Números e dados", "indicadores, gráficos, tabelas e painéis", ["stats", "chart", "table", "onepage", "status", "poster"]],
15
+ ["Mapas", "pontos, linhas e áreas sobre o mapa ao vivo, com a IA buscando dados e fazendo as contas", ["map"], () => mapDemos()],
16
+ ["Imagem e vídeo", "fotos, vídeos e foco guiado", ["image", "full", "spotlight", "carousel", "video"]],
17
+ ["Com a plateia", "perguntas, enquetes e experiências interativas", ["question", "poll", "decisionlab", "playground", "portal"]],
18
+ ["Código e tecnologia", "código, APIs ao vivo, Git e diagramas", ["code", "codewalk", "codelab", "api", "duel", "terminals", "turns", "diagram", "graphlab", "algo", "dossier"]],
19
+ ["Aula e ciência", "fórmulas que viram curva, calculadoras e exercícios", ["science", "calc", "solution"]],
20
+ ["Do seu jeito", "monte o slide livremente", ["blocks", "canvas"]],
21
+ ];
22
+
23
+ // o que não é layout, mas a pessoa precisa saber que existe
24
+ const EXTRAS = [
25
+ ["sparkles", "Criar com IA", "De um tema, de um arquivo (PDF, Word, planilha, PowerPoint) ou de um link: a IA monta a apresentação inteira."],
26
+ ["message-circle", "Conversar com a IA", "No chat, peça mudanças no slide ou no deck todo; cada mudança vira uma versão que dá para desfazer."],
27
+ ["search-check", "Leitura crítica", "A IA revisa a apresentação e aponta o que melhorar, slide a slide, com Aplicar e Ignorar."],
28
+ ["globe", "Pesquisa na web", "Quando o assunto é recente ou de nicho, a IA pesquisa, lê as fontes e cita de onde veio cada dado."],
29
+ ["history", "Versões e desfazer", "Cada mudança, sua ou da IA, vira uma versão: compare e volte quando quiser."],
30
+ ["book-open", "Material de consulta", "Transforme a apresentação de palco em material para a plateia guardar e ler depois."],
31
+ ["file-up", "Importar PowerPoint", "Traga um .pptx e continue editando aqui, com as imagens e a ordem do original."],
32
+ ["table", "Gráfico da planilha", "Cole do Excel ou use a planilha do projeto: o Studio sugere gráficos e atualiza quando o arquivo muda."],
33
+ ["palette", "Temas e paletas", "Troque o visual inteiro com um clique; o conteúdo se rearruma sozinho."],
34
+ ["presentation", "Modo apresentador", "Notas, próximo slide, cronômetro e caneta para desenhar ao vivo."],
35
+ ["download", "PDF e PowerPoint", "Exporte para PDF ou para um PowerPoint editável, com as animações de clique."],
36
+ ["link", "Compartilhar link", "Um link só de leitura, para quem não precisa editar."],
37
+ ];
38
+
39
+ // os exemplos de navegação (hub, cartões) apontam para slides que não existem aqui: o link sai, o desenho fica
40
+ const semLinks = (v) => (Array.isArray(v) ? v.map(semLinks) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([k]) => k !== "goto").map(([k, x]) => [k, semLinks(x)])) : v);
41
+
42
+ const asked = (ask) => `Peça à IA: “${ask}”`;
43
+
44
+ export function tourDeck() {
45
+ const all = Object.keys(LAYOUT_SAMPLES);
46
+ const used = new Set(GROUPS.flatMap(([, , names]) => names));
47
+ const groups = [...GROUPS.map(([title, sub, names, demos]) => [title, sub, names.filter((n) => LAYOUT_SAMPLES[n]), demos])];
48
+ const rest = all.filter((n) => !used.has(n));
49
+ if (rest.length) groups.push(["Mais recursos", "os layouts mais novos", rest]);
50
+
51
+ const slides = [{
52
+ layout: "cover", kicker: "SAGADECK · TOUR", title: TOUR_TITLE,
53
+ subtitle: "Tudo o que dá para fazer, um slide por recurso. Cada um diz como pedir à IA.",
54
+ figure: { icon: "compass", size: 320 },
55
+ notes: "Navegue à vontade: nada aqui cria arquivo. Embaixo de cada slide (nestas notas) está o nome do recurso e a frase para pedir à IA na sua apresentação.",
56
+ }];
57
+ groups.forEach(([title, sub, names, demos], i) => {
58
+ if (!names.length) return;
59
+ const extra = demos ? demos() : [];
60
+ const n = names.length + extra.length;
61
+ slides.push({ layout: "section", number: i + 1, kicker: `${n} recurso${n === 1 ? "" : "s"}`, title, subtitle: sub });
62
+ for (const name of names) {
63
+ const sample = semLinks(structuredClone(LAYOUT_SAMPLES[name]));
64
+ const [label, desc] = LAYOUT_INFO[name] || [name, ""];
65
+ const ask = LAYOUT_ASK[name] || "";
66
+ slides.push({
67
+ ...sample,
68
+ layout: name,
69
+ tour: name,
70
+ source: `**${label}**: ${desc}. ${asked(ask)}`,
71
+ // as notas ficam visíveis logo abaixo do slide no Studio (e na visão do apresentador): valem para todo layout,
72
+ // inclusive os que não desenham o rodapé (capa, citação, vídeo…)
73
+ notes: [`${label}: ${desc}.`, asked(ask), sample.notes].filter(Boolean).join("\n\n"),
74
+ });
75
+ }
76
+ slides.push(...extra);
77
+ });
78
+ slides.push({ layout: "section", number: groups.length + 1, kicker: "Além dos slides", title: "O que mais o SagaDeck faz", subtitle: "IA, importação, exportação e apresentação" });
79
+ slides.push({ layout: "cards", title: "Com a IA", cols: 3, items: EXTRAS.slice(0, 3).map(([icon, title, text]) => ({ icon, title, text })) });
80
+ slides.push({ layout: "cards", title: "A IA pesquisa e guarda", cols: 3, items: EXTRAS.slice(3, 6).map(([icon, title, text]) => ({ icon, title, text })) });
81
+ slides.push({ layout: "cards", title: "Com os seus arquivos", cols: 3, items: EXTRAS.slice(6, 9).map(([icon, title, text]) => ({ icon, title, text })) });
82
+ slides.push({ layout: "cards", title: "Na hora de apresentar", cols: 3, items: EXTRAS.slice(9).map(([icon, title, text]) => ({ icon, title, text })) });
83
+ slides.push({ layout: "end", title: "Agora é a sua vez.", subtitle: "Crie uma apresentação e peça à IA o que você viu aqui." });
84
+ return { title: TOUR_TITLE, theme: "sinal", duration: 30, maxWords: 140, slides };
85
+ }
86
+
87
+ // ---- a demonstração dos mapas (além do exemplo do layout): estados por valor com câmera, linhas e áreas, cobertura
88
+ // com raios calculados pelo código e o que a IA e o Studio fazem. Valores ilustrativos e traçados aproximados, ditos
89
+ // na própria tela; os dados de verdade vêm da planilha, de arquivos ou das buscas da IA.
90
+ const demo = (slide, label, desc, ask) => ({ ...slide, layout: "map", demo: "mapa", source: `**${label}**: ${desc}. ${asked(ask)}`, notes: [`${label}: ${desc}.`, asked(ask)].join("\n\n") });
91
+ const ESTACOES = [
92
+ { nome: "Centro", lat: -23.5489, lon: -46.6388 }, { nome: "Pinheiros", lat: -23.5614, lon: -46.702 },
93
+ { nome: "Ibirapuera", lat: -23.5874, lon: -46.6576 }, { nome: "Mooca", lat: -23.5596, lon: -46.5996 }, { nome: "Santana", lat: -23.5027, lon: -46.6252 },
94
+ ];
95
+ // calculadas uma vez pelo serviço de rotas (OpenRouteService, dados do OpenStreetMap), em 10/10/2026, e gravadas aqui:
96
+ // a rota a pé do MASP ao Parque Ibirapuera e a área alcançável em 15 minutos a pé do MASP. Nada desenhado à mão.
97
+ const ROTA = [[-46.65582,-23.56144],[-46.65663,-23.56304],[-46.65731,-23.56503],[-46.65747,-23.56534],[-46.65836,-23.56627],[-46.65844,-23.5662],[-46.65849,-23.56624],[-46.65847,-23.5663],[-46.65908,-23.56693],[-46.65888,-23.56709],[-46.65891,-23.56717],[-46.66039,-23.56866],[-46.66049,-23.56883],[-46.66022,-23.56911],[-46.66029,-23.56923],[-46.65977,-23.56985],[-46.66047,-23.57058],[-46.6604,-23.57064],[-46.66044,-23.57077],[-46.65929,-23.57188],[-46.65928,-23.57211],[-46.66009,-23.57294],[-46.6601,-23.57307],[-46.65897,-23.57415],[-46.65902,-23.57428],[-46.65957,-23.57482],[-46.65923,-23.57522],[-46.65931,-23.57529],[-46.65833,-23.57626],[-46.65837,-23.57631],[-46.6583,-23.57645],[-46.65882,-23.57697],[-46.65953,-23.57755],[-46.65954,-23.57765],[-46.65966,-23.57768],[-46.66046,-23.57843],[-46.66149,-23.57921],[-46.66151,-23.57933],[-46.6614,-23.57948],[-46.66136,-23.57974],[-46.66108,-23.58011],[-46.66121,-23.58034],[-46.66105,-23.58041],[-46.661,-23.5805],[-46.66124,-23.58067],[-46.66175,-23.58087],[-46.66215,-23.58119],[-46.66237,-23.58158],[-46.66246,-23.58195],[-46.66237,-23.58289],[-46.66228,-23.58305],[-46.6619,-23.58328],[-46.6614,-23.5833],[-46.66122,-23.58341],[-46.66114,-23.58356],[-46.66115,-23.58377],[-46.66147,-23.58427],[-46.66154,-23.58531],[-46.66071,-23.58577],[-46.66056,-23.58616],[-46.66028,-23.58644],[-46.66002,-23.58654],[-46.65852,-23.5868],[-46.65757,-23.58736]];
98
+ const ALCANCE = [[-46.66431,-23.5613],[-46.66432,-23.56766],[-46.66418,-23.5698],[-46.66378,-23.57178],[-46.66291,-23.57199],[-46.66122,-23.57207],[-46.65645,-23.57178],[-46.65422,-23.56956],[-46.64949,-23.56588],[-46.64924,-23.56436],[-46.64736,-23.55976],[-46.64561,-23.55634],[-46.64587,-23.55489],[-46.64616,-23.5541],[-46.64761,-23.55261],[-46.65241,-23.55134],[-46.65448,-23.55097],[-46.65624,-23.55082],[-46.65696,-23.55115],[-46.66012,-23.55392],[-46.66234,-23.55606],[-46.66431,-23.5613]];
99
+ const ROTA_KM = "4,0 km", ROTA_MIN = "48 min";
100
+ const MASP = { nome: "MASP", lat: -23.5614, lon: -46.6559 }, IBIRAPUERA = { nome: "Parque Ibirapuera", lat: -23.5874, lon: -46.6576 };
101
+ // o público de cada região (ilustrativo) dá o tamanho da bolha
102
+ const PUBLICO = [{ nome: "Centro", lat: -23.5489, lon: -46.6388, publico: 42 }, { nome: "Pinheiros", lat: -23.5614, lon: -46.702, publico: 28 },
103
+ { nome: "Mooca", lat: -23.5596, lon: -46.5996, publico: 12 }, { nome: "Santana", lat: -23.5027, lon: -46.6252, publico: 18 }, { nome: "Santo Amaro", lat: -23.6536, lon: -46.7095, publico: 9 }];
104
+
105
+ export function mapDemos() {
106
+ return [
107
+ demo({ kicker: "Bolhas", title: "Onde o público ==está==", basemap: "claro", build: true, caption: "Valores ilustrativos. O tamanho de cada bolha vem da coluna público da planilha.", layers: [
108
+ { name: "Público (mil pessoas)", points: PUBLICO, size: "publico", label: "nome", color: "em" },
109
+ ] }, "Pontos por tamanho", "Uma coluna de números vira o tamanho do ponto; outra coluna vira a cor, com a legenda clicável", "Faça as bolhas do tamanho do público de cada região"),
110
+ demo({ kicker: "Valores por estado", title: "Geração solar por ==estado==", basemap: "claro", build: true, caption: "Valores ilustrativos. Uma região por clique; a câmera vai até ela.", layers: [
111
+ { name: "Sudeste", areas: { SP: 4200, MG: 5100, RJ: 1100, ES: 600 }, legend: "MW", view: "fit" },
112
+ { name: "Nordeste", areas: { BA: 2900, PE: 1200, CE: 1500, RN: 900, PI: 1300 }, legend: "MW", view: "fit" },
113
+ ] }, "Regiões por valor", "Estados ou países pintados pelo número, uma camada por clique, com a câmera indo até cada uma", "Pinte os estados pela potência instalada da planilha e mostre uma região por clique"),
114
+ demo({ kicker: "Linhas e áreas", title: "O trajeto e a ==área== do evento", basemap: "ruas", build: true, caption: "Traçados aproximados, desenhados para a demonstração.", layers: [
115
+ { name: "Trajeto da corrida", color: "em", geojson: { type: "Feature", properties: { nome: "Avenida Paulista" }, geometry: { type: "LineString", coordinates: [[-46.6611, -23.5578], [-46.6559, -23.5613], [-46.6496, -23.5655], [-46.6422, -23.5707]] } } },
116
+ { name: "Área do evento", color: "c3", geojson: { type: "Feature", properties: { nome: "Parque Ibirapuera" }, geometry: { type: "Polygon", coordinates: [[[-46.6618, -23.5833], [-46.6531, -23.5826], [-46.6497, -23.5913], [-46.6586, -23.5952], [-46.6618, -23.5833]]] } } },
117
+ ] }, "Linhas e áreas", "Trajetos, redes e áreas de GeoJSON, GPX ou KML, ou desenhados no Editar mapa", "Desenhe o trajeto da corrida pelas ruas e a área do evento"),
118
+ demo({ kicker: "Rota e alcance", title: "15 minutos a pé do ==MASP==", basemap: "claro", build: true, caption: `Calculados pelo serviço de rotas (OpenRouteService, dados do OpenStreetMap). A pé até o parque: ${ROTA_KM}, cerca de ${ROTA_MIN}.`, layers: [
119
+ { name: "Pontos", points: [MASP, IBIRAPUERA], label: "nome", color: "fg" },
120
+ { name: "Alcance a pé em 15 min", color: "c4", geojson: { type: "Feature", properties: { nome: "15 minutos a pé" }, geometry: { type: "Polygon", coordinates: [ALCANCE] } }, view: "fit" },
121
+ { name: `Rota a pé (${ROTA_KM})`, color: "em", geojson: { type: "Feature", properties: { nome: `Do MASP ao Ibirapuera: ${ROTA_KM}, ${ROTA_MIN}` }, geometry: { type: "LineString", coordinates: ROTA } }, view: "fit" },
122
+ ] }, "Rota pelas ruas e área alcançável", "O caminho de verdade pelas ruas, com a distância e o tempo, e tudo o que se alcança em X minutos a pé, de carro ou de bicicleta", "Mostre o que dá para alcançar em 15 minutos a pé do MASP e a rota até o Ibirapuera"),
123
+ demo({ kicker: "Cobertura", title: "Quem fica a até ==1 km== de uma estação?", basemap: "claro", build: true, caption: "Os raios são calculados pelo sagadeck, não desenhados à mão.", layers: [
124
+ { name: "Estações", points: ESTACOES, label: "nome", color: "em" },
125
+ { name: "Raio de 1 km", color: "c4", geojson: { type: "FeatureCollection", features: ESTACOES.map((e) => ({ type: "Feature", properties: { nome: `${e.nome}: 1 km` }, geometry: circle([e.lat, e.lon], 1000) })) } },
126
+ ] }, "Raio em volta de pontos", "Raio, mais próximo e onde instalar o próximo: o código calcula e a IA explica", "Mostre a área a 1 km de cada estação e quantas escolas ficam fora"),
127
+ demo({ kicker: "Faixa", title: "Quem mora a até ==200 m== da rota?", basemap: "claro", build: true, caption: "A faixa em volta da linha é calculada pelo sagadeck.", layers: [
128
+ { name: "Faixa de 200 m", color: "hi", geojson: { type: "Feature", properties: { nome: "200 m de cada lado", _faixa: true }, geometry: lineBuffer({ type: "LineString", coordinates: ROTA }, 200) } },
129
+ { name: "Rota a pé", color: "em", geojson: { type: "Feature", properties: { nome: "Do MASP ao Ibirapuera" }, geometry: { type: "LineString", coordinates: ROTA } } },
130
+ ] }, "Faixa em volta de uma linha", "Uma rua, uma rede ou um rio com a faixa de X metros dos dois lados; a IA conta o que fica dentro", "Mostre a faixa de 200 metros em volta da rota e quantas escolas ficam nela"),
131
+ { layout: "cards", demo: "mapa", title: "Mapas com a IA", cols: 3, notes: "A IA pede os dados que faltam e o Studio busca, grava na pasta da apresentação com a fonte de cada linha e faz as contas.", items: [
132
+ { icon: "search", title: "Buscar lugares", text: "Cinemas, escolas, unidades de saúde: vêm do OpenStreetMap, com a fonte de cada um." },
133
+ { icon: "map", title: "Contornos e linhas", text: "O bairro, o parque, as ciclovias, o rio: o contorno de verdade, nunca inventado." },
134
+ { icon: "globe", title: "Lista da web", text: "Uma lista com endereços na web vira pontos no mapa, com o link de onde veio." },
135
+ { icon: "map-pin", title: "Coordenadas da planilha", text: "Endereços da sua planilha viram pontos; a IA pergunta antes de enviar." },
136
+ { icon: "ruler", title: "Contas de verdade", text: "O mais próximo, quantos no raio, a faixa em volta de uma linha, onde instalar o próximo." },
137
+ { icon: "route", title: "Rotas e alcance", text: "Tempo a pé, de carro ou de bicicleta, e a área alcançável em X minutos." },
138
+ ] },
139
+ { layout: "spotlight", demo: "mapa", kicker: "No Studio", title: "Onde se ==edita== o mapa", image: "imagens/tour-mapa-botao.jpg",
140
+ caption: "Selecione o slide do mapa: o botão Editar mapa fica no canto do slide. Dois cliques no mapa também abrem.",
141
+ notes: "Editar mapa: o botão no canto do slide de mapa, ou dois cliques no mapa. Abre uma tela só do mapa.",
142
+ hotspots: [{ x: 71.4, y: 29.7, width: 9.8, height: 5.9, title: "Editar mapa", text: "Abre a tela do mapa: camadas, ferramentas e as propriedades de cada item." },
143
+ { x: 24, y: 46, width: 45.5, height: 26, title: "O mapa no slide", text: "Aqui ele só mostra (o slide fica pequeno). Para mexer, Editar mapa ou dois cliques." }] },
144
+ { layout: "spotlight", demo: "mapa", kicker: "No Studio", title: "A tela do ==mapa==", image: "imagens/tour-mapa-editor.jpg",
145
+ caption: "Clique em cada parte para ver o que ela faz.",
146
+ notes: "Editar mapa: camadas à esquerda, ferramentas e o mapa no meio, propriedades à direita, dica embaixo e Concluir no alto.",
147
+ hotspots: [{ x: 0, y: 5.9, width: 15, height: 36, title: "Camadas", text: "Cada camada com o tipo e quantos itens tem; o olho esconde só aqui. Nova camada: pontos, linhas e áreas ou regiões por valor." },
148
+ { x: 15.9, y: 36.7, width: 4.8, height: 32.6, title: "Ferramentas", text: "Selecionar, Ponto, Linha (pode seguir as ruas), Área e Régua." },
149
+ { x: 80, y: 5.9, width: 20, height: 40, title: "Propriedades", text: "Do item clicado: os campos, com o que cada um faz no mapa. Sem nada clicado: a cor, o tamanho, o rótulo e o cartão da camada, e o fundo do mapa." },
150
+ { x: 15, y: 95.1, width: 65, height: 4.9, title: "A dica", text: "Diz o que fazer com a ferramenta escolhida." },
151
+ { x: 92.6, y: 1.1, width: 6.4, height: 3.6, title: "Concluir", text: "Volta ao slide. Tudo já foi gravado." }] },
152
+ { layout: "cards", demo: "mapa", title: "Mapas no Studio", cols: 3, notes: "O mapa funciona sem configurar nada. A chave gratuita do OpenRouteService (Configurar mapa) só acrescenta rotas e área alcançável.", items: [
153
+ { icon: "pencil", title: "Editar mapa", text: "Uma tela só do mapa: camadas à esquerda, o mapa grande no meio e, à direita, o que cada ponto, linha ou área tem." },
154
+ { icon: "table", title: "Planilha ligada", text: "Pôr no mapa a partir da planilha; clicar no ponto mostra a linha, e vice-versa." },
155
+ { icon: "settings", title: "Configurar mapa", text: "Opcional: o servidor de mapas da empresa ou a chave de rotas, como a da IA." },
156
+ ] },
157
+ ];
158
+ }
package/src/themes.js CHANGED
@@ -16,6 +16,15 @@
16
16
  const SYS_SANS = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
17
17
  const SYS_MONO = "'Cascadia Mono', Consolas, 'SF Mono', Menlo, monospace";
18
18
 
19
+ const MUSGO_FACES = {
20
+ display: { css: "font-family: 'Fraunces', Georgia, 'Times New Roman', serif; font-weight: 600; font-optical-sizing: auto; letter-spacing: -0.02em; line-height: 1.04;", pptx: { face: "Georgia", bold: true } },
21
+ heading: { css: "font-family: 'Fraunces', Georgia, serif; font-weight: 600; font-optical-sizing: auto; letter-spacing: -0.015em; line-height: 1.08;", pptx: { face: "Georgia", bold: true } },
22
+ body: { css: "font-family: 'Space Grotesk', 'Segoe UI', sans-serif; font-weight: 400; line-height: 1.42;", pptx: { face: "Segoe UI" }, pptxBold: { face: "Segoe UI Semibold" } },
23
+ label: { css: `font-family: 'IBM Plex Mono', ${SYS_MONO}; font-weight: 500; letter-spacing: 0.25em; text-transform: uppercase; line-height: 1.2;`, pptx: { face: "Cascadia Mono" } },
24
+ mono: { css: `font-family: 'IBM Plex Mono', ${SYS_MONO}; font-weight: 400; line-height: 1.5;`, pptx: { face: "Cascadia Mono" }, pptxBold: { face: "Cascadia Mono SemiBold" } },
25
+ quote: { css: "font-family: 'Fraunces', Georgia, serif; font-style: italic; font-weight: 400; letter-spacing: -0.01em; line-height: 1.14;", pptx: { face: "Georgia", italic: true } },
26
+ };
27
+
19
28
  export const THEMES = {
20
29
  sinal: {
21
30
  label: "Sinal — sinalização/segurança: DIN condensada, amarelo de aviso, pictogramas",
@@ -373,6 +382,151 @@ export const THEMES = {
373
382
  },
374
383
  radius: 6, deco: "none",
375
384
  },
385
+ // Musgo: verde quase preto, âmbar e sálvia, Fraunces + Space Grotesk + IBM Plex Mono (fontes OFL embutidas em
386
+ // runtime/fonts; no PowerPoint, as faces equivalentes do Office). ticker: a capa ganha a faixa que corre;
387
+ // coverAmbient: a rede de nós à deriva atrás da capa. O tom accent
388
+ // (o da seção) continua escuro, com o detalhe em âmbar: o tema não tem página chapada de cor.
389
+ musgo: {
390
+ label: "Musgo — laboratório noturno: verde-musgo quase preto, âmbar e sálvia, serifada Fraunces, chapéu com fio, grade de pontos e faixa correndo na capa",
391
+ colors: {
392
+ paper: "0C0F0B", ink: "ECE8DB", accent: "F0B24A", alert: "E07A55",
393
+ muted: "9FA89A", line: "2B352C", surface: "181F16", surfaceDark: "12160F",
394
+ c1: "F0B24A", c2: "A9C79B", c3: "E07A55", c4: "F6F3E7", c5: "6E8F66",
395
+ },
396
+ tones: {
397
+ light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "accent", em: "accent", onHi: "0C0F0B" },
398
+ dark: { bg: "070906", fg: "F6F3E7", muted: "muted", line: "line", surface: "surfaceDark", hi: "accent", em: "accent", onHi: "0C0F0B" },
399
+ accent: { bg: "12160F", fg: "F6F3E7", muted: "muted", line: "line", surface: "1F2820", hi: "accent", em: "accent", onHi: "0C0F0B" },
400
+ alert: { bg: "12160F", fg: "F6F3E7", muted: "muted", line: "B5502F", surface: "1F2820", hi: "E07A55", em: "E07A55", onHi: "0C0F0B" },
401
+ },
402
+ fontFaces: [],
403
+ faces: MUSGO_FACES,
404
+ radius: 12, deco: "grain", ticker: true, coverAmbient: "rede",
405
+ },
406
+ // Wine: o musgo em vinho — fundo vinho quase preto, vermelho-rosado no destaque e o vermelho forte só no detalhe
407
+ // (tom alert, bolinha da janela). Mesma pele do musgo (skin), mesmas fontes, faixa e rede na capa.
408
+ wine: {
409
+ label: "Wine — o musgo em vinho: fundo vinho quase preto, vermelho-rosado e rosé, serifada Fraunces, faixa correndo e rede na capa",
410
+ skin: "musgo",
411
+ colors: {
412
+ paper: "170A10", ink: "F4EAEC", accent: "F2506A", alert: "CC092F",
413
+ muted: "B59AA4", line: "3D1E2B", surface: "24101A", surfaceDark: "1D0C14",
414
+ c1: "F2506A", c2: "F3A6B5", c3: "CC092F", c4: "FFF5F6", c5: "8C2140",
415
+ },
416
+ tones: {
417
+ light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "accent", em: "accent", onHi: "170A10" },
418
+ dark: { bg: "0F060A", fg: "FFF5F6", muted: "muted", line: "line", surface: "surfaceDark", hi: "accent", em: "accent", onHi: "170A10" },
419
+ accent: { bg: "1D0C14", fg: "FFF5F6", muted: "muted", line: "line", surface: "2E1521", hi: "accent", em: "accent", onHi: "170A10" },
420
+ alert: { bg: "CC092F", fg: "FFFFFF", muted: "FFD0D9", line: "E0475F", surface: "A80726", hi: "FFFFFF", em: "FFFFFF", onHi: "CC092F" },
421
+ },
422
+ fontFaces: [],
423
+ faces: MUSGO_FACES,
424
+ radius: 12, deco: "grain", ticker: true, coverAmbient: "rede",
425
+ },
426
+ // Caderno: visual thinking — papel claro, letra de mão alta (Amatic SC), serifada de leitura (Gelasio), cartões
427
+ // brancos com moldura grossa em tons pastel (menta, lilás, amarelo, coral) e rabiscos de ícones nos cantos.
428
+ caderno: {
429
+ label: "Caderno — visual thinking: papel claro, letra de mão alta, cartões com moldura pastel e rabiscos de ícones",
430
+ colors: {
431
+ paper: "F5F3EE", ink: "2B2A33", accent: "8E7896", alert: "DE8178",
432
+ muted: "6E6A73", line: "D9D5CC", surface: "FFFFFF", surfaceDark: "2B2A33",
433
+ c1: "B5E3C3", c2: "8E7896", c3: "F0D488", c4: "DE8178", c5: "9CC9E8",
434
+ },
435
+ tones: {
436
+ light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "F0D488", em: "alert", onHi: "ink" },
437
+ dark: { bg: "2B2A33", fg: "F5F3EE", muted: "B7B3BC", line: "4A4853", surface: "37353F", hi: "F0D488", em: "F0D488", onHi: "ink" },
438
+ accent: { bg: "EFEADF", fg: "ink", muted: "muted", line: "D3CCBD", surface: "FFFFFF", hi: "B5E3C3", em: "alert", onHi: "ink" },
439
+ alert: { bg: "DE8178", fg: "FFFFFF", muted: "FBE3E0", line: "E9A39C", surface: "C96C63", hi: "F0D488", em: "2B2A33", onHi: "ink" },
440
+ },
441
+ fontFaces: [],
442
+ faces: {
443
+ display: { css: "font-family: 'Amatic SC', 'Segoe Print', cursive; font-weight: 700; letter-spacing: 0.01em; line-height: 0.95;", pptx: { face: "Ink Free", bold: true } },
444
+ heading: { css: "font-family: 'Amatic SC', 'Segoe Print', cursive; font-weight: 700; letter-spacing: 0.02em; line-height: 1;", pptx: { face: "Ink Free", bold: true } },
445
+ body: { css: "font-family: 'Gelasio', Georgia, serif; font-weight: 400; line-height: 1.45;", pptx: { face: "Georgia" } },
446
+ label: { css: "font-family: 'Gelasio', Georgia, serif; font-weight: 700; letter-spacing: 0.02em; line-height: 1.2;", pptx: { face: "Georgia", bold: true } },
447
+ mono: { css: `font-family: ${SYS_MONO}; line-height: 1.4;`, pptx: { face: "Cascadia Mono" } },
448
+ quote: { css: "font-family: 'Gelasio', Georgia, serif; font-style: italic; font-weight: 500; line-height: 1.2;", pptx: { face: "Georgia", italic: true } },
449
+ },
450
+ radius: 26, deco: "grain",
451
+ },
452
+ // Lousa: quadro-negro verde com giz — título em giz amarelo (Cabin Sketch), texto em letra redonda (Itim), traços de
453
+ // giz laranja e amarelo, cartões contornados a giz.
454
+ lousa: {
455
+ label: "Lousa — quadro verde com giz: títulos em giz amarelo, traços laranja, cartões contornados a giz",
456
+ colors: {
457
+ paper: "2E4A3E", ink: "F1F0E8", accent: "E9DE7A", alert: "E58A5C",
458
+ muted: "B8C4BB", line: "5E7A6C", surface: "36574A", surfaceDark: "263E34",
459
+ c1: "E9DE7A", c2: "E58A5C", c3: "9FD3E0", c4: "F1F0E8", c5: "C7A6D9",
460
+ },
461
+ tones: {
462
+ light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "accent", em: "accent", onHi: "2E4A3E" },
463
+ dark: { bg: "1F3329", fg: "ink", muted: "muted", line: "line", surface: "surfaceDark", hi: "accent", em: "accent", onHi: "2E4A3E" },
464
+ accent: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "alert", em: "accent", onHi: "2E4A3E" },
465
+ alert: { bg: "4A3A2C", fg: "F1F0E8", muted: "D8C9B4", line: "6E5A46", surface: "5A4735", hi: "E9DE7A", em: "E58A5C", onHi: "2E4A3E" },
466
+ },
467
+ fontFaces: [],
468
+ faces: {
469
+ display: { css: "font-family: 'Cabin Sketch', 'Segoe Print', cursive; font-weight: 700; letter-spacing: 0.01em; line-height: 1;", pptx: { face: "Ink Free", bold: true } },
470
+ heading: { css: "font-family: 'Cabin Sketch', 'Segoe Print', cursive; font-weight: 700; letter-spacing: 0.01em; line-height: 1.05;", pptx: { face: "Ink Free", bold: true } },
471
+ body: { css: "font-family: 'Itim', 'Segoe UI', sans-serif; font-weight: 400; line-height: 1.4;", pptx: { face: "Segoe UI" } },
472
+ label: { css: "font-family: 'Itim', 'Segoe UI', sans-serif; font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; line-height: 1.2;", pptx: { face: "Segoe UI" } },
473
+ mono: { css: `font-family: ${SYS_MONO}; line-height: 1.4;`, pptx: { face: "Cascadia Mono" } },
474
+ quote: { css: "font-family: 'Itim', 'Segoe UI', sans-serif; font-weight: 400; line-height: 1.25;", pptx: { face: "Segoe UI" } },
475
+ },
476
+ radius: 14, deco: "none",
477
+ },
478
+ // Cartaz: cartaz de protesto — papel cinza com grão, condensada pesada (Bebas Neue) em faixas vermelhas tortas,
479
+ // etiquetas amarelas, cartões brancos levemente girados como fotos coladas, moldura preta grossa na capa.
480
+ cartaz: {
481
+ label: "Cartaz — cartaz de protesto: condensada em faixas vermelhas tortas, etiquetas amarelas, cartões como fotos coladas",
482
+ colors: {
483
+ paper: "E8E6E1", ink: "232323", accent: "D9565B", alert: "F2BE4E",
484
+ muted: "5E5B57", line: "C9C5BE", surface: "FFFFFF", surfaceDark: "2B2B2B",
485
+ c1: "D9565B", c2: "8CC8BF", c3: "F2BE4E", c4: "C3C7F0", c5: "232323",
486
+ },
487
+ tones: {
488
+ light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "accent", em: "accent", onHi: "FFFFFF" },
489
+ dark: { bg: "232323", fg: "E8E6E1", muted: "A9A49C", line: "3D3D3D", surface: "surfaceDark", hi: "accent", em: "alert", onHi: "FFFFFF" },
490
+ accent: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "accent", em: "accent", onHi: "FFFFFF" },
491
+ alert: { bg: "F2BE4E", fg: "232323", muted: "6B5420", line: "D9A43A", surface: "F6CF77", hi: "232323", em: "D9565B", onHi: "F2BE4E" },
492
+ },
493
+ fontFaces: [],
494
+ faces: {
495
+ display: { css: "font-family: 'Bebas Neue', 'Bahnschrift Condensed', 'Arial Narrow', sans-serif; font-weight: 400; letter-spacing: 0.01em; line-height: 0.9;", pptx: { face: "Bahnschrift Condensed", bold: true } },
496
+ heading: { css: "font-family: 'Bebas Neue', 'Bahnschrift Condensed', 'Arial Narrow', sans-serif; font-weight: 400; letter-spacing: 0.015em; line-height: 0.95;", pptx: { face: "Bahnschrift Condensed", bold: true } },
497
+ body: { css: `font-family: ${SYS_SANS}; font-weight: 400; line-height: 1.38;`, pptx: { face: "Segoe UI" }, pptxBold: { face: "Segoe UI Semibold" } },
498
+ label: { css: "font-family: 'Bebas Neue', 'Bahnschrift Condensed', sans-serif; font-weight: 400; letter-spacing: 0.06em; line-height: 1.1;", pptx: { face: "Bahnschrift Condensed" } },
499
+ mono: { css: `font-family: ${SYS_MONO}; line-height: 1.4;`, pptx: { face: "Cascadia Mono" } },
500
+ quote: { css: "font-family: 'Bebas Neue', 'Bahnschrift Condensed', sans-serif; font-style: italic; font-weight: 400; letter-spacing: 0.01em; line-height: 1.02;", pptx: { face: "Bahnschrift Condensed", italic: true } },
501
+ },
502
+ radius: 0, deco: "grain",
503
+ },
504
+ // Infográfico: cartaz de dados chapado — azul, verde, laranja e amarelo sem degradê, números enormes em condensada
505
+ // (Bebas Neue), títulos em Montserrat pesada, faixas de cor, números em círculos coloridos.
506
+ infografico: {
507
+ label: "Infográfico — dados chapados: azul, verde, laranja e amarelo, números enormes, faixas de cor e marcadores em círculo",
508
+ colors: {
509
+ paper: "FFFFFF", ink: "2E3138", accent: "3F86C9", alert: "EB6C35",
510
+ muted: "6B7280", line: "E3E6EA", surface: "F2F4F6", surfaceDark: "2E3138",
511
+ c1: "3F86C9", c2: "8DC63F", c3: "EB6C35", c4: "F5C02F", c5: "3AAFC1",
512
+ },
513
+ tones: {
514
+ light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "F5C02F", em: "accent", onHi: "ink" },
515
+ dark: { bg: "2E3138", fg: "FFFFFF", muted: "B9BEC7", line: "454953", surface: "3A3E46", hi: "8DC63F", em: "8DC63F", onHi: "2E3138" },
516
+ accent: { bg: "accent", fg: "FFFFFF", muted: "D6E6F5", line: "6AA3D8", surface: "3577B5", hi: "8DC63F", em: "F5C02F", onHi: "2E3138" },
517
+ alert: { bg: "8DC63F", fg: "FFFFFF", muted: "EEF7E2", line: "A7D46C", surface: "7DB234", hi: "2E3138", em: "2E3138", onHi: "FFFFFF" },
518
+ },
519
+ fontFaces: [],
520
+ faces: {
521
+ display: { css: "font-family: 'Bebas Neue', 'Bahnschrift Condensed', 'Arial Narrow', sans-serif; font-weight: 400; letter-spacing: 0.01em; line-height: 0.9;", pptx: { face: "Bahnschrift Condensed", bold: true } },
522
+ heading: { css: "font-family: 'Montserrat', 'Segoe UI', sans-serif; font-weight: 800; letter-spacing: -0.01em; line-height: 1.05;", pptx: { face: "Segoe UI Black" } },
523
+ body: { css: "font-family: 'Montserrat', 'Segoe UI', sans-serif; font-weight: 500; line-height: 1.4;", pptx: { face: "Segoe UI" }, pptxBold: { face: "Segoe UI Semibold" } },
524
+ label: { css: "font-family: 'Montserrat', 'Segoe UI', sans-serif; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; line-height: 1.2;", pptx: { face: "Segoe UI Semibold" } },
525
+ mono: { css: `font-family: ${SYS_MONO}; line-height: 1.4;`, pptx: { face: "Cascadia Mono" } },
526
+ quote: { css: "font-family: 'Montserrat', 'Segoe UI', sans-serif; font-weight: 700; letter-spacing: -0.01em; line-height: 1.2;", pptx: { face: "Segoe UI Semibold" } },
527
+ },
528
+ radius: 10, deco: "none",
529
+ },
376
530
  // Par técnico (material de consulta, tutorial, apostila): o claro imprime bem, o escuro descansa a vista.
377
531
  // pair: o Studio alterna entre os dois (Design › Claro/Escuro) e o PDF pode sair no claro (Preferências).
378
532
  manual: {
@@ -1,28 +1,8 @@
1
- title: Texto no cenário e matemática
1
+ title: Matemática interativa
2
2
  id: cenario-e-ciencia
3
3
  theme: aurora
4
4
  motion: none
5
5
  slides:
6
- - layout: scenography
7
- scene: stage
8
- kicker: IDEIAS QUE OCUPAM ESPAÇO
9
- title: "ALÉM DO\nÓBVIO"
10
- subtitle: O texto também faz parte da história.
11
- caption: SAGADECK / ESTUDO 01
12
- deco: none
13
- - layout: scenography
14
- scene: floor
15
- kicker: UMA NOVA PERSPECTIVA
16
- title: "NOVOS\nCAMINHOS"
17
- caption: SAGADECK / ESTUDO 02
18
- deco: none
19
- - layout: scenography
20
- scene: signs
21
- kicker: PRÓXIMA PARADA
22
- title: "IDEIAS\nEM CENA"
23
- subtitle: Mensagens que pertencem ao lugar.
24
- caption: SAGADECK / ESTUDO 03
25
- deco: none
26
6
  - layout: science
27
7
  title: A onda e sua derivada
28
8
  equations:
@@ -1,5 +1,5 @@
1
1
  # Ensaio de aula de APIs de IA: roda contra a API de mentira, no ambiente embutido ENSAIO do Studio
2
- # (biblioteca → Nova → "Exemplo: aula de APIs ao vivo", ou `sagadeck ensaio-api`).
2
+ # (`sagadeck ensaio-api`; no Studio, a demo "APIs com curl" mostra as chamadas mais comuns).
3
3
  # Serve de modelo: troque as URLs e os caminhos $.… pelos do seu serviço e cadastre os seus ambientes
4
4
  # no Studio (Inserir → Ambientes), que grava em ~/.sagadeck/ambientes.yaml. Veja "Slide api" em docs/REFERENCIA.md.
5
5
  title: "Ensaio: APIs de IA ao vivo"
@@ -125,8 +125,8 @@ slides:
125
125
  title: Qual frase fica ==mais perto==?
126
126
  request: { url: "{{base}}/embeddings", body: { model: emb, input: "{{text}}" } }
127
127
  similarity:
128
- reference: Quero abrir uma conta no banco
129
- texts: [ "Vai chover amanhã em São Paulo?", "Como faço para abrir uma conta?", "Abrir conta no banco hoje" ]
128
+ reference: Quero reservar uma mesa no restaurante
129
+ texts: [ "Vai chover amanhã em São Paulo?", "Como faço para reservar uma mesa?", "Mesa para dois esta noite" ]
130
130
  notes: "Peça frases para a plateia e escreva na aba Frases."
131
131
 
132
132
  - layout: api