sagadeck 1.7.0 → 1.8.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 (65) hide show
  1. package/README.md +41 -1
  2. package/SKILL.md +3 -3
  3. package/bin/sagadeck.js +2 -2
  4. package/docs/ANDAMENTO.md +162 -4
  5. package/docs/EXPERIENCIAS.md +1 -1
  6. package/docs/REFERENCIA.md +59 -1
  7. package/package.json +2 -1
  8. package/src/ai/deck-ai.js +13 -4
  9. package/src/ai/ia-config.js +17 -2
  10. package/src/ai/llm.js +6 -4
  11. package/src/ai/map-analysis-run.js +119 -0
  12. package/src/ai/map-data.js +221 -0
  13. package/src/ai/variety.js +1 -1
  14. package/src/build.js +6 -2
  15. package/src/experiences.js +1 -1
  16. package/src/export/browser.js +23 -1
  17. package/src/export/pptx.js +2 -1
  18. package/src/export/shots.js +3 -1
  19. package/src/layouts.js +10 -0
  20. package/src/library.js +2 -2
  21. package/src/map-analysis.js +106 -0
  22. package/src/map-config.js +83 -0
  23. package/src/map-routing.js +82 -0
  24. package/src/map-services.js +60 -0
  25. package/src/map.js +261 -0
  26. package/src/mcp/server.js +4 -4
  27. package/src/runtime/base.css +21 -1
  28. package/src/runtime/code-lab.js +10 -10
  29. package/src/runtime/map.js +234 -0
  30. package/src/runtime/runtime.js +1 -1
  31. package/src/runtime/skins/aluminio.css +1 -1
  32. package/src/runtime/skins/prata.css +1 -1
  33. package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
  34. package/src/runtime/vendor/geo-contornos.json +1 -0
  35. package/src/runtime/vendor/leaflet.css +661 -0
  36. package/src/runtime/vendor/leaflet.js +6 -0
  37. package/src/studio/ai-chat-route.js +20 -0
  38. package/src/studio/banner.js +1 -1
  39. package/src/studio/demo-decks.js +3 -1
  40. package/src/studio/geo-routes.js +138 -0
  41. package/src/studio/ia-routes.js +4 -1
  42. package/src/studio/layout-samples.js +71 -0
  43. package/src/studio/project.js +1 -1
  44. package/src/studio/public/ai-settings.js +15 -2
  45. package/src/studio/public/app.js +13 -7
  46. package/src/studio/public/chat.js +1 -1
  47. package/src/studio/public/explorer.js +23 -8
  48. package/src/studio/public/index.html +4 -0
  49. package/src/studio/public/library.css +2 -1
  50. package/src/studio/public/library.html +2 -0
  51. package/src/studio/public/library.js +10 -1
  52. package/src/studio/public/map-data.js +154 -0
  53. package/src/studio/public/map-editor.js +287 -0
  54. package/src/studio/public/map-fields.js +21 -0
  55. package/src/studio/public/map-settings.js +84 -0
  56. package/src/studio/public/slide-form.js +1 -1
  57. package/src/studio/public/style.css +13 -0
  58. package/src/studio/public/ui-icons.js +1 -1
  59. package/src/studio/public/viewers.js +2 -1
  60. package/src/studio/public/visual-editor.js +1 -1
  61. package/src/studio/server.js +13 -1
  62. package/src/studio/tour.js +76 -0
  63. package/src/templates/scaffold.js +5 -5
  64. package/src/themes.js +5 -5
  65. package/templates/{exemplo-keynote.yaml → exemplo-prata.yaml} +6 -6
@@ -243,7 +243,7 @@
243
243
  // ------------------------------------------------------------------ planilha como no Excel
244
244
  const colName = (i) => { let s = ""; i++; while (i > 0) { const r = (i - 1) % 26; s = String.fromCharCode(65 + r) + s; i = Math.floor((i - 1) / 26); } return s; };
245
245
  const looksNum = (v) => /^\s*[-+]?(R\$\s*)?(\d{1,3}(\.\d{3})+(,\d+)?|\d+([.,]\d+)?)\s*%?\s*$/.test(String(v ?? ""));
246
- function sheetGrid(host, { rows, editable = false, hasHeader = false, maxRender = 3000, onChange, types = [] } = {}) {
246
+ function sheetGrid(host, { rows, editable = false, hasHeader = false, maxRender = 3000, onChange, onSelect, types = [] } = {}) {
247
247
  let data = rows.map((r) => [...r]);
248
248
  const undo = [], redo = [];
249
249
  let sel = { r: 0, c: 0, r2: 0, c2: 0 }, editing = null;
@@ -267,6 +267,7 @@
267
267
  const [r1, r2] = [Math.min(sel.r, sel.r2), Math.max(sel.r, sel.r2)], [c1, c2] = [Math.min(sel.c, sel.c2), Math.max(sel.c, sel.c2)];
268
268
  for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) cell(r, c)?.classList.add("sel");
269
269
  const cur = cell(sel.r, sel.c); cur?.classList.add("cur");
270
+ onSelect?.(sel.r, sel.c); // quem abriu a planilha acompanha a linha (ex.: o mapa abre o cartão do ponto)
270
271
  for (let c = c1; c <= c2; c++) table.querySelector(`thead th[data-col="${c}"]`)?.classList.add("on");
271
272
  for (let r = r1; r <= r2; r++) table.querySelector(`tbody th[data-row="${r}"]`)?.classList.add("on");
272
273
  cur?.scrollIntoView({ block: "nearest", inline: "nearest" });
@@ -257,7 +257,7 @@
257
257
  if(!root.dataset.visualEvents){
258
258
  root.dataset.visualEvents='true';
259
259
  root.addEventListener('pointerdown',e=>{
260
- if(e.button!==0 || e.target.closest('.science-plot,input,textarea'))return;
260
+ if(e.button!==0 || e.target.closest('.science-plot,.map-live,input,textarea'))return; // gráfico e mapa ao vivo recebem o próprio clique
261
261
  if(e.target.closest('[contenteditable="true"]')?.dataset.writing)return; // escrevendo: o clique é do cursor
262
262
  const el=objectAt(e.clientX,e.clientY)||(locked(e.target.closest('[data-vkey]'))?null:e.target.closest('[data-vkey]'));
263
263
  if(!el){
@@ -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";
@@ -493,6 +494,13 @@ export function createStudioServer(deckPath = null, opts = {}) {
493
494
  res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/diagram.js" ? "diagram.js" : "vendor/mermaid.min.js")));
494
495
  return;
495
496
  }
497
+ // mapa ao vivo (src/runtime/map.js): o Leaflet e os contornos só são pedidos quando um slide de mapa monta
498
+ if (["/map.js", "/leaflet.js", "/leaflet.css", "/geo-contornos.json"].includes(pathname)) {
499
+ const type = pathname.endsWith(".css") ? "text/css" : pathname.endsWith(".json") ? "application/json" : "application/javascript";
500
+ res.writeHead(200, { "Content-Type": `${type}; charset=utf-8` }); // sem cache: depois de atualizar, o mapa novo vale na hora
501
+ res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/map.js" ? "map.js" : `vendor${pathname}`)));
502
+ return;
503
+ }
496
504
  if (["/science.js", "/formula.js", "/calc.js", "/plotly.min.js"].includes(pathname)) {
497
505
  res.writeHead(200, {"Content-Type":"application/javascript; charset=utf-8"});
498
506
  res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/plotly.min.js" ? "vendor/plotly.min.js" : pathname.slice(1))));
@@ -895,12 +903,16 @@ export function createStudioServer(deckPath = null, opts = {}) {
895
903
 
896
904
  // Configurar IA (provedor, chave, modelos desta máquina): src/studio/ia-routes.js
897
905
  if (pathname.startsWith("/api/ia") && await iaRoutes({ req, res, pathname, opts, readJSON })) return;
906
+ // Configurar mapa, estado dos serviços e coordenadas pelo endereço: src/studio/geo-routes.js
907
+ if (pathname.startsWith("/api/mapa") && await geoRoutes({ req, res, pathname, opts, readJSON, W, isBundledTemplate })) return;
898
908
 
899
909
  if (pathname === "/api/ai/status" && req.method === "GET") {
900
910
  const cfg = llmConfig();
901
911
  const available = await llmAvailable({ force: url.searchParams.has("refresh") });
902
912
  res.writeHead(200, { "Content-Type": "application/json" });
903
- res.end(JSON.stringify({ available, configured: llmConfigured(cfg), source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
913
+ // no servidor, só se está ligada: provedor e modelos são de quem administra (src/studio/ia-routes.js)
914
+ if (opts.multiuser) { res.end(JSON.stringify({ available, configured: llmConfigured(cfg), server: true })); return; }
915
+ res.end(JSON.stringify({ available, configured: llmConfigured(cfg), problem: cfg.problem || "", source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
904
916
  return;
905
917
  }
906
918
 
@@ -0,0 +1,76 @@
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
+
6
+ export const TOUR_TITLE = "Conheça o SagaDeck";
7
+
8
+ // os grupos na ordem em que fazem sentido para quem está conhecendo; layout que não estiver em nenhum entra em "Mais"
9
+ const GROUPS = [
10
+ ["Abrir e fechar", "capa, seções, agenda e encerramento", ["cover", "section", "agenda", "end", "references"]],
11
+ ["Uma ideia por vez", "frases, números e palavras que ocupam o palco", ["statement", "headline", "quote", "definition", "number", "kinetic", "scenography"]],
12
+ ["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"]],
13
+ ["Números e dados", "indicadores, gráficos, tabelas e painéis", ["stats", "chart", "table", "map", "onepage", "status", "poster"]],
14
+ ["Imagem e vídeo", "fotos, vídeos e foco guiado", ["image", "full", "spotlight", "carousel", "video"]],
15
+ ["Com a plateia", "perguntas, enquetes e experiências interativas", ["question", "poll", "decisionlab", "playground", "portal"]],
16
+ ["Código e tecnologia", "código, APIs ao vivo, Git e diagramas", ["code", "codewalk", "codelab", "api", "duel", "terminals", "turns", "diagram", "graphlab", "algo", "dossier"]],
17
+ ["Aula e ciência", "fórmulas que viram curva, calculadoras e exercícios", ["science", "calc", "solution"]],
18
+ ["Do seu jeito", "monte o slide livremente", ["blocks", "canvas"]],
19
+ ];
20
+
21
+ // o que não é layout, mas a pessoa precisa saber que existe
22
+ const EXTRAS = [
23
+ ["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."],
24
+ ["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."],
25
+ ["search-check", "Leitura crítica", "A IA revisa a apresentação e aponta o que melhorar, slide a slide, com Aplicar e Ignorar."],
26
+ ["file-up", "Importar PowerPoint", "Traga um .pptx e continue editando aqui, com as imagens e a ordem do original."],
27
+ ["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."],
28
+ ["palette", "Temas e paletas", "Troque o visual inteiro com um clique; o conteúdo se rearruma sozinho."],
29
+ ["presentation", "Modo apresentador", "Notas, próximo slide, cronômetro e caneta para desenhar ao vivo."],
30
+ ["download", "PDF e PowerPoint", "Exporte para PDF ou para um PowerPoint editável, com as animações de clique."],
31
+ ["link", "Compartilhar link", "Um link só de leitura, para quem não precisa editar."],
32
+ ];
33
+
34
+ // os exemplos de navegação (hub, cartões) apontam para slides que não existem aqui: o link sai, o desenho fica
35
+ 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);
36
+
37
+ const asked = (ask) => `Peça à IA: “${ask}”`;
38
+
39
+ export function tourDeck() {
40
+ const all = Object.keys(LAYOUT_SAMPLES);
41
+ const used = new Set(GROUPS.flatMap(([, , names]) => names));
42
+ const groups = [...GROUPS.map(([title, sub, names]) => [title, sub, names.filter((n) => LAYOUT_SAMPLES[n])])];
43
+ const rest = all.filter((n) => !used.has(n));
44
+ if (rest.length) groups.push(["Mais recursos", "os layouts mais novos", rest]);
45
+
46
+ const slides = [{
47
+ layout: "cover", kicker: "SAGADECK · TOUR", title: TOUR_TITLE,
48
+ subtitle: "Tudo o que dá para fazer, um slide por recurso. Cada um diz como pedir à IA.",
49
+ figure: { icon: "compass", size: 320 },
50
+ 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.",
51
+ }];
52
+ groups.forEach(([title, sub, names], i) => {
53
+ if (!names.length) return;
54
+ slides.push({ layout: "section", number: i + 1, kicker: `${names.length} recurso${names.length === 1 ? "" : "s"}`, title, subtitle: sub });
55
+ for (const name of names) {
56
+ const sample = semLinks(structuredClone(LAYOUT_SAMPLES[name]));
57
+ const [label, desc] = LAYOUT_INFO[name] || [name, ""];
58
+ const ask = LAYOUT_ASK[name] || "";
59
+ slides.push({
60
+ ...sample,
61
+ layout: name,
62
+ tour: name,
63
+ source: `**${label}**: ${desc}. ${asked(ask)}`,
64
+ // as notas ficam visíveis logo abaixo do slide no Studio (e na visão do apresentador): valem para todo layout,
65
+ // inclusive os que não desenham o rodapé (capa, citação, vídeo…)
66
+ notes: [`${label}: ${desc}.`, asked(ask), sample.notes].filter(Boolean).join("\n\n"),
67
+ });
68
+ }
69
+ });
70
+ 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" });
71
+ slides.push({ layout: "cards", title: "Com a IA", cols: 3, items: EXTRAS.slice(0, 3).map(([icon, title, text]) => ({ icon, title, text })) });
72
+ slides.push({ layout: "cards", title: "Com os seus arquivos", cols: 3, items: EXTRAS.slice(3, 6).map(([icon, title, text]) => ({ icon, title, text })) });
73
+ slides.push({ layout: "cards", title: "Na hora de apresentar", cols: 3, items: EXTRAS.slice(6).map(([icon, title, text]) => ({ icon, title, text })) });
74
+ slides.push({ layout: "end", title: "Agora é a sua vez.", subtitle: "Crie uma apresentação e peça à IA o que você viu aqui." });
75
+ return { title: TOUR_TITLE, theme: "sinal", duration: 30, maxWords: 140, slides };
76
+ }
@@ -6,7 +6,7 @@ export function generateScaffold({
6
6
  title = "Minha Apresentação",
7
7
  theme = "prata",
8
8
  author = "Seu Nome",
9
- type = "keynote",
9
+ type = "lancamento",
10
10
  } = {}) {
11
11
  const normType = String(type).toLowerCase();
12
12
 
@@ -243,7 +243,7 @@ export function generateScaffold({
243
243
  };
244
244
  }
245
245
 
246
- // Padrão: Keynote Clean / Apple style
246
+ // Padrão: lançamento, limpo e com muito respiro (tema prata)
247
247
  return {
248
248
  title,
249
249
  author,
@@ -252,7 +252,7 @@ export function generateScaffold({
252
252
  slides: [
253
253
  {
254
254
  layout: "cover",
255
- kicker: "Keynote de Lançamento",
255
+ kicker: "Lançamento",
256
256
  title: "Design com ==Propósito==.",
257
257
  subtitle: "A harmonia entre simplicidade, respiro e precisão",
258
258
  author,
@@ -355,14 +355,14 @@ export function generateScaffold({
355
355
  text: "O padrão definitivo para criadores exigentes.",
356
356
  rating: 5,
357
357
  ratingLabel: "5.0 Pro",
358
- tags: ["Keynote", "Prata"],
358
+ tags: ["Lançamento", "Prata"],
359
359
  },
360
360
  ],
361
361
  notes: "> O tom escuro cria um momento teatral no palco.",
362
362
  },
363
363
  {
364
364
  layout: "end",
365
- title: "Pense Diferente.",
365
+ title: "Simples assim.",
366
366
  subtitle: "Obrigado.",
367
367
  contacts: ["apresentacao@empresa.com", "suporte@empresa.com"],
368
368
  notes: "> Conclusão elegante e contato.",
package/src/themes.js CHANGED
@@ -270,7 +270,7 @@ export const THEMES = {
270
270
  },
271
271
 
272
272
  prata: {
273
- label: "Prata — Keynote clean & espacial: prata acetinado, muito respiro, tipografia SF / Inter limpa e elegante",
273
+ label: "Prata — limpo e espacial: prata acetinado, muito respiro, tipografia do sistema / Inter limpa e elegante",
274
274
  colors: {
275
275
  paper: "F5F5F7", ink: "1D1D1F", accent: "0071E3", alert: "FF3B30",
276
276
  muted: "86868B", line: "D2D2D7", surface: "FFFFFF", surfaceDark: "161617",
@@ -293,10 +293,10 @@ export const THEMES = {
293
293
  },
294
294
  radius: 20, deco: "silver",
295
295
  },
296
- // Alumínio: a cara dos lançamentos de iPhone e iPad de 2020-2021 (prata metálico, alumínio escovado, títulos
297
- // cromados, grafite no escuro). O prata segue fiel ao branco da Apple de hoje.
296
+ // Alumínio: a cara de lançamento de aparelho premium (prata metálico, alumínio escovado, títulos
297
+ // cromados, grafite no escuro). O prata segue o branco limpo.
298
298
  aluminio: {
299
- label: "Alumínio — lançamento de iPhone e iPad (2020): prata metálico, títulos cromados, alumínio escovado e grafite",
299
+ label: "Alumínio — lançamento de aparelho premium: prata metálico, títulos cromados, alumínio escovado e grafite",
300
300
  colors: {
301
301
  paper: "E4E5E9", ink: "1C1C1E", accent: "0066CC", alert: "D70015",
302
302
  muted: "5E5E63", line: "C3C5CA", surface: "F4F5F7", surfaceDark: "2C2C2E",
@@ -305,7 +305,7 @@ export const THEMES = {
305
305
  tones: {
306
306
  light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "D1D3D8", em: "accent", onHi: "ink" },
307
307
  dark: { bg: "1C1C1E", fg: "F5F5F7", muted: "A1A1A6", line: "3A3A3C", surface: "surfaceDark", hi: "48484A", em: "2997FF", onHi: "FFFFFF" },
308
- // azul-pacífico do iPhone 12 Pro, metálico (a pele põe o brilho)
308
+ // azul-pacífico metálico (a pele põe o brilho)
309
309
  accent: { bg: "1D3A50", fg: "FFFFFF", muted: "C5D6E3", line: "3F6580", surface: "2A4C64", hi: "FFFFFF", em: "8FC1E3", onHi: "1D3A50" },
310
310
  alert: { bg: "alert", fg: "FFFFFF", muted: "FFD1D4", line: "F0525F", surface: "B30012", hi: "FFFFFF", em: "FFFFFF", onHi: "alert" },
311
311
  },
@@ -1,10 +1,10 @@
1
1
  title: Inovação & Design
2
- author: Keynote Clean
2
+ author: Estúdio Prata
3
3
  theme: prata
4
4
  duration: 12
5
5
  slides:
6
6
  - layout: cover
7
- kicker: Apresentação Especial · Keynote
7
+ kicker: Apresentação Especial · Lançamento
8
8
  title: Simplicidade com ==Impacto==.
9
9
  subtitle: A estética do respiro, elegância e precisão visual
10
10
  author: Design Team
@@ -79,12 +79,12 @@ slides:
79
79
  title: "Padrão de Mercado"
80
80
  text: "A referência definitiva para profissionais sérios."
81
81
  rating: 5
82
- ratingLabel: "5.0 Keynote"
83
- tags: ["Keynote", "Prata", "Apple Clean"]
82
+ ratingLabel: "5.0 Pro"
83
+ tags: ["Lançamento", "Prata", "Clean"]
84
84
 
85
85
  - layout: end
86
- title: Pense Diferente.
86
+ title: Simples assim.
87
87
  subtitle: Obrigado.
88
88
  contacts:
89
- - apple.com/keynote
89
+ - design.org
90
90
  - contato@design.org