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
@@ -22,11 +22,10 @@ export const LAYOUT_INFO = {
22
22
  science: ["Fórmulas e funções", "Escreva a fórmula e ela vira curva; cada letra vira um controle deslizante. Aceita pontos colados do Excel e superfície 3D"],
23
23
  infographic: ["Infográfico", "Arco, ramos, lados, trilhas ou metrô: de 2 a 8 itens coloridos em volta de um centro"],
24
24
  diagram: ["Diagrama", "Fluxograma, sequência, estados, UML, jornada, mapa mental (Mermaid) nas cores do tema"],
25
- scenography: ["Texto no cenário", "13 composições (palco, terminal, café, embarque, pregão, cinema, revista…), sem animação"],
26
25
  cover: ["Capa", "Abertura com título, subtítulo e autor"],
27
26
  section: ["Seção", "Abre um novo ato ou capítulo"],
28
27
  statement: ["Frase de impacto", "Uma ideia em letras grandes"],
29
- calendar: ["Calendário", "Datas num calendário: um cartão por mês com o mini calendário e os eventos marcados; o que não tem data vai em \"sem data\""],
28
+ calendar: ["Calendário", "Datas num calendário: um cartão por mês, um mês grande de parede, a semana com horários ou a agenda em lista; com muitos eventos, um slide por mês"],
30
29
  poster: ["Pôster (infográfico)", "Uma página como figura de revista científica: ilustração principal e painéis com letra (a, b, c), desenho, texto curto e números"],
31
30
  definition: ["Definição", "O termo em destaque, a origem da palavra em partes e a definição, com um ícone ilustrativo"],
32
31
  headline: ["Manchete", "Uma frase enorme ocupando o slide"],
@@ -48,6 +47,7 @@ export const LAYOUT_INFO = {
48
47
  timeline: ["Linha do tempo", "Eventos em sequência"],
49
48
  chart: ["Gráfico de dados", "Barras, colunas, linhas, rosca: cole a tabela do Excel ou digite os valores"],
50
49
  table: ["Tabela", "Tabela de verdade nas cores do tema: cole do Excel, destaque linha e total"],
50
+ map: ["Mapa", "Pontos, linhas e áreas sobre o mapa ao vivo: da planilha do projeto, de GeoJSON/GPX/KML ou valores por estado; cor por categoria, cartão ao clicar, uma camada por clique"],
51
51
  compare: ["Comparação", "A × B lado a lado"],
52
52
  matrix: ["Matriz 2×2", "Quatro quadrantes"],
53
53
  question: ["Pergunta", "Pergunta para a plateia com timer"],
@@ -57,7 +57,6 @@ export const LAYOUT_INFO = {
57
57
  codewalk: ["Código guiado", "Explique uma linha por vez, com destaques e saída simulada"],
58
58
  api: ["API ao vivo", "Requisição que executa na hora (tipo Postman): síncrono, polling ou streaming, com código curl/Python"],
59
59
  spotlight: ["Foco guiado", "Conduza o olhar por detalhes de uma imagem ou diagrama"],
60
- kinetic: ["Tipografia cinética", "Frases em sequência viram parte da cena com ritmo e estilos tipográficos que mudam"],
61
60
  video: ["Vídeo", "Player YouTube online ou MP4 incorporado"],
62
61
  blocks: ["Livre (blocos)", "Monte com blocos em linhas e colunas"],
63
62
  canvas: ["Livre (posições)", "Posicione cada elemento à mão"],
@@ -65,7 +64,75 @@ export const LAYOUT_INFO = {
65
64
  end: ["Encerramento", "Obrigado e contatos"],
66
65
  };
67
66
 
67
+ // Como pedir cada layout à IA, em português comum: aparece no "Conheça o SagaDeck" (src/studio/tour.js), para quem vê
68
+ // o recurso saber a frase que o gera. Exemplos de áreas variadas, nunca de um setor só.
69
+ export const LAYOUT_ASK = {
70
+ portal: "Abra a palestra entrando numa porta que transforma o cenário e revela o tema",
71
+ playground: "Faça um slide interativo em que a plateia mexe num controle e vê a cor do céu mudar",
72
+ codelab: "Monte um exercício de código em que os alunos alteram a entrada e executam passo a passo",
73
+ graphlab: "Mostre a rede de metrô da cidade e deixe testar a rota mais curta tirando uma estação",
74
+ mosaic: "Liste as seis frentes do projeto num mosaico que se ajusta sozinho",
75
+ ribbon: "Mostre as três fases da obra em cápsulas, uma ao lado da outra",
76
+ dossier: "Faça uma página de consulta com os comandos e as respostas da API, para guardar",
77
+ decisionlab: "Compare revisão humana e automação com premissas que eu possa ajustar ao vivo",
78
+ solution: "Resolva passo a passo quanto rende uma aplicação de R$ 1.000 a 1% ao mês em três anos",
79
+ calc: "Faça uma calculadora do consumo de energia de uma casa, com faixas de baixo, médio e alto",
80
+ algo: "Mostre a busca binária rodando de verdade, com o código e as variáveis",
81
+ carousel: "Apresente cinco destinos de viagem, um por clique, com foto e uma frase",
82
+ duel: "Simule duas pessoas editando o mesmo arquivo no Git até dar conflito",
83
+ terminals: "Mostre dois terminais lado a lado: um faz push e o outro puxa a mudança",
84
+ turns: "Encene um diálogo entre cliente e atendente, uma fala por clique",
85
+ science: "Mostre a parábola y = ax² + bx + c com controles para a, b e c",
86
+ infographic: "Faça um infográfico com os cinco pilares da sustentabilidade em volta de um centro",
87
+ diagram: "Desenhe o fluxo de atendimento de um pedido, da chegada à entrega",
88
+ cover: "Faça a capa da palestra sobre energia solar, com o meu nome e a data",
89
+ section: "Abra a segunda parte, sobre os resultados",
90
+ statement: "Uma frase de impacto: a água mais barata é a que não se desperdiça",
91
+ calendar: "Mostre num calendário as datas das oficinas do semestre",
92
+ poster: "Faça um pôster de uma página explicando como funciona uma placa solar",
93
+ definition: "Defina \"sustentabilidade\", com a origem da palavra",
94
+ headline: "Uma manchete enorme: 40% da água tratada se perde no caminho",
95
+ quote: "Ponha a citação de Paulo Freire sobre ensinar, com o autor",
96
+ onepage: "Resuma o projeto numa página: jornada, problema com números, solução e painel",
97
+ status: "Faça o status da semana: feito, em andamento, bloqueios e próximos passos",
98
+ hub: "Monte um menu em que a plateia escolhe o caminho: iniciante, intermediário ou avançado",
99
+ number: "Mostre o número 1,2 milhão de visitantes, contando até ele",
100
+ split: "Explique o ciclo da água com o texto de um lado e uma ilustração do outro",
101
+ full: "Uma foto de floresta ocupando o slide inteiro",
102
+ cards: "Três cartões com os benefícios do programa, cada um com ícone",
103
+ bento: "Monte um mosaico de blocos com os destaques do ano",
104
+ stats: "Mostre quatro indicadores do trimestre com a tendência de cada um",
105
+ steps: "Mostre em etapas como instalar um sensor de umidade",
106
+ funnel: "Faça o funil de inscrições: visitantes, cadastros, inscritos e concluintes",
107
+ pyramid: "Monte a pirâmide de prioridades, do essencial ao desejável",
108
+ list: "Liste os cinco cuidados antes de uma viagem longa",
109
+ agenda: "Faça a agenda do evento com a parte atual destacada",
110
+ timeline: "Faça a linha do tempo da história do cinema brasileiro",
111
+ chart: "Faça um gráfico de linhas com a temperatura média de cada mês",
112
+ table: "Monte a tabela comparando três planos, com o total destacado",
113
+ map: "Mostre no mapa as estações de medição da planilha, coloridas pelo status, com um cartão ao clicar",
114
+ compare: "Compare transporte público e carro próprio lado a lado",
115
+ matrix: "Monte uma matriz 2x2 de esforço e impacto com as ideias do time",
116
+ question: "Faça uma pergunta para a plateia com cronômetro de 30 segundos",
117
+ poll: "Faça uma enquete ao vivo: qual tema vocês querem aprofundar?",
118
+ image: "Ponha esta foto em tela cheia com uma legenda curta",
119
+ code: "Mostre este código Python com as linhas da conexão destacadas",
120
+ codewalk: "Explique este código uma linha por vez, com a saída simulada",
121
+ api: "Faça uma chamada de API ao vivo que consulta o clima e mostra a resposta",
122
+ spotlight: "Conduza o olhar pelas partes desta planta baixa, uma por vez",
123
+ video: "Ponha este vídeo do YouTube no slide",
124
+ blocks: "Monte livremente um slide com título, dois textos e uma imagem em colunas",
125
+ canvas: "Posicione o logotipo, o título e a foto onde eu indicar",
126
+ references: "Liste as referências bibliográficas no fim",
127
+ end: "Feche com um obrigado e os meus contatos",
128
+ };
129
+
68
130
  export const LAYOUT_SAMPLES = {
131
+ map: { layout: "map", kicker: "Monitoramento", title: "Estações de ==qualidade do ar==", basemap: "claro", layers: [
132
+ { name: "Estações", color: "status", label: "nome", points: [
133
+ { nome: "Centro", lat: -23.5489, lon: -46.6388, status: "Boa", pm25: 12 }, { nome: "Pinheiros", lat: -23.5614, lon: -46.7020, status: "Moderada", pm25: 24 },
134
+ { nome: "Ibirapuera", lat: -23.5874, lon: -46.6576, status: "Boa", pm25: 9 }, { nome: "Mooca", lat: -23.5596, lon: -46.5996, status: "Ruim", pm25: 41 },
135
+ { nome: "Santana", lat: -23.5027, lon: -46.6252, status: "Moderada", pm25: 27 }], colors: { Ruim: "alert" } }] },
69
136
  portal: portalExample,
70
137
  playground: playgroundExample,
71
138
  codelab: codeLabExample,
@@ -74,8 +141,8 @@ export const LAYOUT_SAMPLES = {
74
141
  ribbon: {layout:"ribbon",title:"Do primeiro passo ao resultado",items:[{title:"Explorar",text:"Encontre uma oportunidade."},{title:"Testar",text:"Experimente em pequena escala."},{title:"Evoluir",text:"Repita o que funciona."}]},
75
142
  dossier: {layout:"dossier",title:"Referência rápida",items:[{title:"Requisição",text:"GET consulta o recurso.",code:"GET /exemplos/42"},{title:"Resposta",text:"Confira o status antes de usar o corpo.",code:'{"id":42,"status":"ok"}'}]},
76
143
  decisionlab: {layout:"decisionlab", title:"A revisão melhora o resultado?", lab:{volume:10000,errorRate:2,reviewRate:100,catchRate:60,introducedRate:0.2,seconds:30}},
77
- infographic: { layout: "infographic", shape: "arco", title: "Os ==desafios== do projeto", center: { title: "Desafios" }, items: [{ title: "Acesso", text: "Pedido pela Bridge" }, { title: "Dados", text: "Mockados no experimento" }, { title: "Segurança", text: "Chaves no Key Vault" }, { title: "Infra", text: "Namespace e firewall" }] },
78
- diagram: { layout: "diagram", kicker: "Processo", title: "Da ideia à ==produção==", mermaid: "flowchart LR\n A([:lightbulb: Ideia]):::hi --> B{Vale testar?}\n B -->|sim| C[:flask-conical: Experimento]\n B -.->|não| X[Arquivar]:::vazado\n C ==> D[:rocket: Produção]:::em" },
144
+ infographic: { layout: "infographic", shape: "arco", title: "Os ==desafios== da viagem", center: { title: "Desafios" }, items: [{ title: "Roteiro", text: "Três cidades em dez dias" }, { title: "Orçamento", text: "Hospedagem pesa mais" }, { title: "Documentos", text: "Passaporte e visto" }, { title: "Clima", text: "Chuva na segunda semana" }] },
145
+ diagram: { layout: "diagram", kicker: "Processo", title: "Da ideia à ==produção==", mermaid: "flowchart LR\n A([:lightbulb: Ideia]):::hi --> B{Vale testar?}\n B -->|sim| C[:flask-conical: Protótipo]\n B -.->|não| X[Arquivar]:::vazado\n C ==> D[:rocket: Produção]:::em" },
79
146
  solution: {layout:"solution", title:"Exercício: quanto rende em 3 anos?", problem:"R$ 1.000 aplicados a 1% ao mês, com juros compostos. Quanto há depois de 36 meses?", givens:[{symbol:"C", value:"1000", unit:"R\\$", label:"capital"}, {symbol:"i", value:"0,01", label:"taxa ao mês"}, {symbol:"n", value:"36", label:"meses"}], find:"M", steps:[{text:"Juros compostos", latex:"M = C\\,(1 + i)^n"}, {text:"Substitui os dados", latex:"M = 1000 \\cdot 1{,}01^{36}"}, {text:"Calcula a potência", latex:"1{,}01^{36} \\approx 1{,}4308"}], answer:{latex:"M \\approx R\\$\\,1.430{,}77", text:"Rende cerca de 43% em 3 anos."}},
80
147
  calc: {layout:"calc", title:"Qual é o regime do escoamento?", inputs:{V:{label:"Velocidade", latex:"V", value:1.5, min:0.01, max:4, step:0.01, unit:"m/s"}, D:{label:"Diâmetro", latex:"D", value:0.05, min:0.005, max:0.3, step:0.005, unit:"m"}, nu:{label:"Viscosidade (água, 20 °C)", latex:"\\nu = 1{,}0\\times10^{-6}\\ \\mathrm{m^2/s}", value:0.000001, fixed:true}}, outputs:[{name:"Re", label:"Número de Reynolds", latex:"Re = \\frac{VD}{\\nu}", fn:"V*D/nu", decimals:0}, {name:"regime", label:"Regime", of:"Re", cases:[{below:2300, text:"Laminar", color:"s3"}, {below:4000, text:"Transição", color:"em"}, {text:"Turbulento", color:"hi"}], scale:{min:100, max:1000000, log:true}}]},
81
148
  algo: {layout:"algo", title:"Bubble sort passo a passo", algorithm:"bubble", array:[5, 1, 4, 2, 8, 3]},
@@ -86,15 +153,14 @@ export const LAYOUT_SAMPLES = {
86
153
  terminals: {layout:"terminals", title:"Dois terminais", file:"README.md", base:"# Projeto\nversão 1", people:["Ana","Beto"],
87
154
  turns:[{who:1, edit:{2:"versão 2"}, commit:"sobe a versão"}, {who:1, push:true}, {who:2, pull:true}, {who:2, log:true}]},
88
155
  turns: {layout:"turns", title:"Code review ao vivo", people:[{name:"Ana", role:"autora do PR"}, {name:"Beto", role:"revisor"}],
89
- turns:[{who:1, text:"Abri o PR com a validação do Pix."}, {who:2, text:"Por que não usa o **schema** que já existe?", tag:"pergunta"}, {who:1, text:"Boa. Troquei e testei assim:", method:"POST", url:"/pix", status:201, code:'{ "valor": 10 }', language:"json"}, {who:2, text:"Aprovado.", tag:"aprovado"}]},
156
+ turns:[{who:1, text:"Abri o PR com a validação das inscrições."}, {who:2, text:"Por que não usa o **schema** que já existe?", tag:"pergunta"}, {who:1, text:"Boa. Troquei e testei assim:", method:"POST", url:"/inscricoes", status:201, code:'{ "nome": "Ana" }', language:"json"}, {who:2, text:"Aprovado.", tag:"aprovado"}]},
90
157
  science: {layout:"science", title:"Mexa no a e no b", equations:[{label:"A função",latex:"f(x) = a\\sin(bx)"},{label:"A derivada",latex:"f'(x) = ab\\cos(bx)"}], plot:{functions:["a*sin(b*x)","a*b*cos(b*x)"], x:[-6.3,6.3], params:{a:{value:1.5,min:0,max:3,label:"amplitude"},b:{value:1,min:0.5,max:3,label:"frequência"}}}},
91
- scenography: {layout:"scenography", scene:"stage", title:"ALÉM DO\nÓBVIO", kicker:"UMA NOVA PERSPECTIVA", subtitle:"Ideias que ocupam espaço.", tone:"dark", deco:"none"},
92
158
  cover: { layout: "cover", kicker: "Evento 2026", title: "Título da ==apresentação==", subtitle: "Um subtítulo curto", author: "Seu nome", figure: { icon: "rocket", size: 320 } },
93
159
  section: { layout: "section", number: 2, kicker: "Parte dois", title: "O problema" },
94
160
  statement: { layout: "statement", kicker: "A ideia", text: "Uma frase que ==muda tudo==." },
95
161
  calendar: { layout: "calendar", kicker: "Agenda", title: "Próximos lançamentos", events: [
96
162
  { date: "2026-11-12", title: "Beta aberto", tag: "Produto", text: "Para os 500 primeiros" }, { date: "2026-11-28", title: "Evento de lançamento", tag: "Evento" },
97
- { date: "2027-01-15", title: "Versão 2.0", tag: "Produto", text: "Com o app de celular" }], undated: [{ title: "Integração com o banco", text: "Em negociação" }] },
163
+ { date: "2027-01-15", title: "Versão 2.0", tag: "Produto", text: "Com o app de celular" }], undated: [{ title: "Loja em outra cidade", text: "Em negociação" }] },
98
164
  poster: { layout: "poster", kicker: "Receita", title: "Pão de fermentação natural", subtitle: "Do fermento ao forno em quatro etapas", flow: true,
99
165
  panels: [
100
166
  { title: "Fermento", icon: "wheat", text: "Farinha e água, alimentados por 5 dias", facts: [{ value: "1:1", label: "farinha e água" }] },
@@ -105,9 +171,9 @@ export const LAYOUT_SAMPLES = {
105
171
  definition: { layout: "definition", kicker: "O que é", term: "Hidrologia", origin: "do grego", parts: [{ word: "hydor", meaning: "água" }, { word: "logos", meaning: "ciência" }], text: "É a ciência que estuda ==a água sobre a Terra==: sua quantidade, distribuição e circulação, e sua relação com o meio ambiente e com os seres vivos.", icon: "droplet" },
106
172
  headline: { layout: "headline", text: "78% ==desistem==", caption: "no segundo passo" },
107
173
  quote: { layout: "quote", quote: "Dados vencem opiniões.", by: "Alguém sábio" },
108
- onepage: { layout: "onepage", title: "Cadastro digital", journey: [{ icon: "user", title: "Cliente chega", text: "Fila de 40 min" }, { icon: "file-text", title: "Preenche papel" }, { icon: "check", title: "Conta aberta", text: "Até 5 dias" }], problem: { text: "Cadastro em papel atrasa a abertura de contas", numbers: [{ value: "5 dias", label: "para abrir uma conta" }] }, solution: { text: "Cadastro no tablet, com assinatura digital", items: ["Conta aberta no mesmo dia"] } },
109
- status: { layout: "status", kicker: "Semana 39", title: "Portal do cliente", health: "risco", progress: 65, done: ["Tela de login", "Integração com o cadastro"], doing: ["Recuperação de senha"], blocked: [{ text: "Liberação de firewall", owner: "Infra", due: "30/09" }], upcoming: ["Homologar com o negócio"] },
110
- hub: { layout: "hub", title: "Qual é o seu caso?", question: "Cada caminho tem o seu prazo", options: [{ icon: "flask-conical", title: "Experimento", meta: "30 dias", goto: "exp" }, { icon: "rocket", title: "Projeto", meta: "DEV, HOM e PROD", goto: "proj" }] },
174
+ onepage: { layout: "onepage", title: "Agendamento online", journey: [{ icon: "user", title: "Paciente liga", text: "Espera de 15 min" }, { icon: "file-text", title: "Preenche a ficha" }, { icon: "check", title: "Consulta marcada", text: "Até 5 dias" }], problem: { text: "Marcar por telefone atrasa as consultas", numbers: [{ value: "5 dias", label: "para marcar uma consulta" }] }, solution: { text: "Agendamento pelo celular, com confirmação automática", items: ["Consulta marcada no mesmo dia"] } },
175
+ status: { layout: "status", kicker: "Semana 39", title: "Portal do cliente", health: "risco", progress: 65, done: ["Tela de login", "Integração com o cadastro"], doing: ["Recuperação de senha"], blocked: [{ text: "Aprovação do texto de privacidade", owner: "Jurídico", due: "30/09" }], upcoming: ["Teste com 20 usuários"] },
176
+ hub: { layout: "hub", title: "Qual é o seu caso?", question: "Cada caminho tem o seu ritmo", options: [{ icon: "user", title: "Primeira vez", meta: "4 passos", goto: "inicio" }, { icon: "rocket", title: "Já uso", meta: "Atalhos", goto: "atalhos" }] },
111
177
  number: { layout: "number", value: 42, suffix: "%", label: "mais rápido" },
112
178
  split: { layout: "split", title: "Texto e figura", bullets: ["Primeiro ponto", "Segundo ponto"], figure: { icon: "lightbulb", size: 300 } },
113
179
  full: { layout: "full", figure: { chart: "donut", value: 72, center: "72%" }, title: "Figura em página inteira", overlay: "left" },
@@ -138,11 +204,7 @@ export const LAYOUT_SAMPLES = {
138
204
  hotspots: [{ x: 4, y: 18, width: 92, height: 15, title: "O pedido", text: "O verbo GET consulta o recurso. A rota identifica a coleção de aulas." },
139
205
  { x: 4, y: 38, width: 92, height: 10, title: "A confirmação", text: "200 OK confirma o sucesso. O tempo ajuda a investigar a experiência do usuário." },
140
206
  { x: 5, y: 52, width: 78, height: 39, title: "Os dados", text: "A estrutura JSON é o contrato que o front-end precisa entender." }] },
141
- kinetic: { layout: "kinetic", figure: { svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080"><defs><linearGradient id="stage" x2="0" y2="1"><stop stop-color="#697a78"/><stop offset="1" stop-color="#26353a"/></linearGradient><linearGradient id="window" x2="0" y2="1"><stop stop-color="#e1cba1"/><stop offset="1" stop-color="#8d9990"/></linearGradient></defs><path fill="url(#stage)" d="M0 0h1920v1080H0z"/><path fill="#202b2e" d="M0 0h430v1080H0zM1490 0h430v1080h-430z"/><path fill="url(#window)" d="M500 90h910v660H500z"/><path fill="#d0b88e" opacity=".4" d="M500 560h910v190H500z"/><path fill="#293432" d="M0 850h1920v230H0z"/><path fill="#394342" d="M720 814h480v36H720z"/><path fill="#26302f" d="M836 555c28-120 205-154 266-14l76 263H760z"/><circle cx="960" cy="465" r="74" fill="#39403b"/></svg>' }, autoplay: true, interval: 1800,
142
- beats: [{ text: "Uma ideia", style: "clean", position: "left", color: "white", size: "medium", tag: "GANHA FORMA" },
143
- { text: "muda o olhar", style: "clean", position: "right", color: "gold", size: "medium" },
144
- { text: "e abre caminhos", style: "clean", position: "left", color: "white", size: "medium" }] },
145
- // genérico de propósito: troque método, URL e corpo pelos do seu serviço (os exemplos prontos ficam no deck "Exemplo: aula de APIs")
207
+ // genérico de propósito: troque método, URL e corpo pelos do seu serviço (os exemplos prontos ficam na demo "APIs com curl" e no `sagadeck ensaio-api`)
146
208
  api: { layout: "api", kicker: "Ao vivo", title: "Minha ==chamada==", text: "Troque o método, a URL e o corpo pelos do seu serviço", request: { method: "POST", url: "{{base}}/echo", body: { mensagem: "Olá, API" } }, answer: "$.recebido.mensagem" },
147
209
  video: { layout: "video", title: "Assista", url: "https://example.com", label: "Assistir" },
148
210
  blocks: { layout: "blocks", title: "Blocos livres", content: [{ row: [{ lead: "Esquerda" }, { lead: "Direita" }] }] },
@@ -103,15 +103,6 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
103
103
  if (!made) throw new Error("Não há um modelo em prévia aberto.");
104
104
  return ok(made);
105
105
  }
106
- case "/api/library/decks/example-cenario": {
107
- // demonstração do Texto no cenário: o YAML e as imagens de exemplo (fundo e recorte transparente)
108
- const dir0 = TEMPLATE_DIRS.map((d) => path.join(d, "cenario")).find((d) => fs.existsSync(d));
109
- if (!dir0) throw new Error("o exemplo não veio no pacote (templates/cenario)");
110
- const tpl = fs.readdirSync(dir0).find((f) => f.endsWith(".yaml"));
111
- const id = L.createDeck(b.topic || "", YAML.parse(fs.readFileSync(path.join(dir0, tpl), "utf8")));
112
- fs.cpSync(path.join(dir0, "imagens"), path.join(path.dirname(L.resolveId(id)), "imagens"), { recursive: true, force: false });
113
- return ok({ id });
114
- }
115
106
  case "/api/library/decks/example": {
116
107
  // exemplo de slides api (aula de APIs de IA): roda no ambiente embutido "ensaio", sem configurar nada
117
108
  const tpl = templateFile("ensaio-api.yaml");
@@ -20,7 +20,7 @@ const toNum = (v) => rawNum(String(v ?? "").trim().replace(/^R\$\s*/i, "").repla
20
20
  export const META = ".sagadeck";
21
21
  export const CONTEXT = "contexto";
22
22
  const TEXT_EXT = new Set(["md", "markdown", "txt", "csv", "tsv", "json", "yaml", "yml", "css", "js", "mjs", "cjs", "jsx", "ts", "tsx", "html", "htm", "svg", "xml",
23
- "py", "r", "m", "sql", "sh", "bash", "bat", "ps1", "ini", "toml", "cfg", "java", "c", "h", "cpp", "cs", "go", "rs", "tex", "bib", "log"]);
23
+ "py", "r", "m", "sql", "sh", "bash", "bat", "ps1", "ini", "toml", "cfg", "java", "c", "h", "cpp", "cs", "go", "rs", "tex", "bib", "log", "geojson", "gpx", "kml"]); // geo: as camadas do mapa (map.js) se editam aqui
24
24
  const SHEET_EXT = new Set(["csv", "tsv", "xlsx"]);
25
25
  const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg"]);
26
26
  const extOf = (f) => (String(f).match(/\.([a-z0-9]+)$/i)?.[1] || "").toLowerCase();
@@ -226,6 +226,7 @@ export function writeSheet(P, rel, rows) {
226
226
  if (!Array.isArray(rows) || rows.some((r) => !Array.isArray(r))) throw new Error("Planilha inválida.");
227
227
  const old = fs.existsSync(abs) ? parseCSV(fs.readFileSync(abs, "utf8"), ext === "tsv" ? "\t" : undefined) : { delimiter: ext === "tsv" ? "\t" : ",", eol: "\n" };
228
228
  const tmp = abs + ".tmp";
229
+ fs.mkdirSync(path.dirname(abs), { recursive: true }); // planilha nova numa pasta que ainda não existe (contexto/)
229
230
  const clean = rows.map((r) => r.map((c) => String(c ?? "")));
230
231
  while (clean.length > 1 && clean.at(-1).every((c) => c === "")) clean.pop(); // linha vazia no fim (o cursor passou dela) não vai para o arquivo
231
232
  fs.writeFileSync(tmp, toCSV(clean, old));
@@ -8,13 +8,15 @@ window.SagaAISettings = (() => {
8
8
  .ai-cfg .row{display:flex;gap:8px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap}.ai-cfg button{padding:7px 14px;border-radius:8px;border:1px solid var(--border,#c9ced6);background:transparent;color:inherit;cursor:pointer;font:inherit}
9
9
  .ai-cfg .rec{background:var(--selected,#eef);border-radius:8px;padding:8px 10px;margin:6px 0 4px}.ai-cfg .rec button{margin-top:6px;display:block}
10
10
  .ai-cfg button.primary{background:var(--brand,#5b4bdb);border-color:var(--brand,#5b4bdb);color:#fff}.ai-cfg [data-status]{margin-top:10px;min-height:1.4em}.ai-cfg [data-status].ok{color:#15803d}.ai-cfg [data-status].err{color:#b91c1c}`;
11
- const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web" };
12
- const HINT = { text: "obrigatório", vision: "opcional; vazio = o de texto", image: "opcional; para gerar ilustrações", search: "opcional; no OpenRouter, um modelo :online" };
11
+ const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web", video: "Modelo de vídeo" };
12
+ const HINT = { text: "obrigatório", vision: "opcional; vazio = o de texto", image: "opcional; para gerar ilustrações", search: "opcional; no OpenRouter, um modelo :online", video: "opcional; para gerar clipes (cobrado por segundo)" };
13
13
  const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
14
14
  let dialog, info, onSaved = () => {};
15
15
 
16
+ // o estilo da caixa serve também ao Configurar mapa (map-settings.js)
17
+ const ensureStyle = () => { if (!document.getElementById("ai-cfg-style")) { const st = document.createElement("style"); st.id = "ai-cfg-style"; st.textContent = css; document.head.append(st); } };
16
18
  function build() {
17
- if (!document.getElementById("ai-cfg-style")) { const st = document.createElement("style"); st.id = "ai-cfg-style"; st.textContent = css; document.head.append(st); }
19
+ ensureStyle();
18
20
  dialog = document.createElement("dialog");
19
21
  dialog.className = "ai-cfg";
20
22
  dialog.id = "ai-settings-dialog";
@@ -85,6 +87,7 @@ window.SagaAISettings = (() => {
85
87
  }
86
88
 
87
89
  return {
90
+ ensureStyle,
88
91
  async open(saved) {
89
92
  onSaved = typeof saved === "function" ? saved : () => {};
90
93
  if (!dialog) build();
@@ -35,7 +35,7 @@
35
35
  const LAYOUT_NAMES = [
36
36
  "cover", "section", "statement", "definition", "poster", "calendar", "headline", "quote", "number", "split", "full",
37
37
  "cards", "bento", "mosaic", "ribbon", "stats", "steps", "funnel", "pyramid", "list", "agenda", "timeline", "hub", "status", "onepage",
38
- "chart", "table", "compare", "matrix", "decisionlab", "diagram", "infographic", "question", "poll", "image", "code", "codewalk", "spotlight", "scenography", "portal", "science", "kinetic", "video", "carousel", "duel", "terminals", "turns", "solution", "calc", "algo", "graphlab", "codelab", "playground",
38
+ "chart", "table", "map", "compare", "matrix", "decisionlab", "diagram", "infographic", "question", "poll", "image", "code", "codewalk", "spotlight", "portal", "science", "video", "carousel", "duel", "terminals", "turns", "solution", "calc", "algo", "graphlab", "codelab", "playground",
39
39
  "blocks", "dossier", "canvas", "references", "end",
40
40
  ];
41
41
 
@@ -43,9 +43,9 @@
43
43
  const LAYOUT_LABELS = {
44
44
  cover: "Capa", section: "Seção", statement: "Frase de impacto", definition: "Definição", poster: "Pôster (infográfico)", calendar: "Calendário", quote: "Citação", number: "Número grande",
45
45
  split: "Texto e figura", cards: "Cartões", stats: "Indicadores", steps: "Etapas", list: "Lista",
46
- timeline: "Linha do tempo", chart: "Gráfico de dados", table: "Tabela", compare: "Comparação", matrix: "Matriz 2×2",
46
+ timeline: "Linha do tempo", chart: "Gráfico de dados", table: "Tabela", map: "Mapa", compare: "Comparação", matrix: "Matriz 2×2",
47
47
  question: "Pergunta", poll: "Enquete", image: "Imagem", code: "Código", video: "Vídeo",
48
- diagram: "Diagrama", infographic: "Infográfico", mosaic: "Grade adaptável", ribbon: "Cápsulas", dossier: "Página de consulta", decisionlab: "Laboratório de decisões", science: "Fórmulas e funções", scenography: "Texto no cenário", codewalk: "Código guiado", spotlight: "Foco guiado", kinetic: "Tipografia cinética",
48
+ diagram: "Diagrama", infographic: "Infográfico", mosaic: "Grade adaptável", ribbon: "Cápsulas", dossier: "Página de consulta", decisionlab: "Laboratório de decisões", science: "Fórmulas e funções", codewalk: "Código guiado", spotlight: "Foco guiado",
49
49
  duel: "Duelo de commits", terminals: "Dois terminais", turns: "Turnos a dois", carousel: "Carrossel", solution: "Exercício resolvido", calc: "Calculadora ao vivo", algo: "Algoritmo animado",
50
50
  graphlab: "Rede explorável", codelab: "Código explorável", playground: "Experiência personalizada", portal: "Portal e transformação",
51
51
  blocks: "Livre (blocos)", canvas: "Livre (posições)", end: "Encerramento", references: "Referências",
@@ -394,6 +394,7 @@
394
394
  const idx = state.currentSlideIndex;
395
395
  const slide = state.deck.slides[idx];
396
396
  if (!slide) return;
397
+ window.SagaProject?.slideChanged?.(slide);
397
398
  if (dom.tabPanelVars.classList.contains("active")) renderStudioSavedVars();
398
399
 
399
400
  renderReviewBar(slide, idx);
@@ -424,8 +425,11 @@
424
425
 
425
426
  // Renderizar HTML no palco
426
427
  window.SagaScience?.dispose(dom.renderedSlideContainer);
428
+ window.SagaMap?.dispose(dom.renderedSlideContainer, { keepView: true });
427
429
  dom.renderedSlideContainer.innerHTML = data.html;
428
430
  window.SagaScience?.mount(dom.renderedSlideContainer);
431
+ window.SagaMap?.mount(dom.renderedSlideContainer); // só aqui (prévia grande); miniaturas e galeria ficam na prévia em SVG
432
+ window.SagaMapData?.attach(dom.renderedSlideContainer, slide, () => { syncDeckToServer(); renderCurrentSlide(); }, { toast: showToast, hydrate: hydrateIcons, materialized: previewMaterialized });
429
433
  window.SagaCalc?.mount(dom.renderedSlideContainer); // calculadora ao vivo: os controles funcionam no editor também
430
434
  window.SagaDiagrams?.mount(dom.renderedSlideContainer).then(() => reportDiagram(dom.renderedSlideContainer));
431
435
  window.SagaDecisionLab?.mount(dom.renderedSlideContainer);
@@ -2045,11 +2049,11 @@
2045
2049
  const SLIDE_GROUPS = [
2046
2050
  ["estrutura", "Abertura e estrutura", ["cover", "section", "agenda", "end", "references"]],
2047
2051
  ["texto", "Texto e ideias", ["statement", "definition", "poster", "calendar", "headline", "quote", "list", "cards", "split", "mosaic", "ribbon", "bento", "dossier"]],
2048
- ["dados", "Números e gráficos", ["number", "stats", "chart", "table", "science", "compare", "matrix"]],
2052
+ ["dados", "Números e gráficos", ["number", "stats", "chart", "table", "map", "science", "compare", "matrix"]],
2049
2053
  ["processo", "Processos e diagramas", ["steps", "timeline", "funnel", "pyramid", "diagram", "infographic", "hub"]],
2050
2054
  ["gestao", "Gestão e status", ["status", "onepage", "decisionlab"]],
2051
2055
  ["codigo", "Código e API", ["code", "codewalk", "api"]],
2052
- ["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "scenography", "portal", "kinetic"]],
2056
+ ["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "portal"]],
2053
2057
  ["plateia", "Plateia", ["question", "poll"]],
2054
2058
  ["dinamicas", "Dinâmicas a dois", ["duel", "terminals", "turns"]],
2055
2059
  ["aula", "Aula: exercícios e algoritmos", ["solution", "calc", "algo", "graphlab", "codelab", "playground"]],
@@ -2325,38 +2329,13 @@
2325
2329
 
2326
2330
  // Ao trocar de layout, o conteúdo em lista (cartões, etapas, eventos, níveis…) vai junto
2327
2331
  const LIST_KEY = { cards: "items", bento: "tiles", stats: "stats", steps: "steps", funnel: "stages", pyramid: "levels", list: "items",
2328
- agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts", kinetic: "beats" };
2332
+ agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts" };
2329
2333
  const SOURCE_KEYS = ["items", "tiles", "stats", "kpis", "steps", "process", "flow", "stages", "levels", "events", "cells", "options", "bullets", "lines", "contacts"];
2330
- function kineticPhrases(value) {
2331
- const words = String(value || "").match(/==[^=]+==|\*\*.*?\*\*|\*[^*]+\*|~~.*?~~|`[^`]+`|\[[^\]]+\]\([^)]+\)|\S+/g) || [];
2332
- const connectors = new Set(["a", "as", "de", "do", "da", "dos", "das", "e", "em", "no", "na", "nos", "nas", "para", "por", "com", "o", "os"]);
2333
- const phrases = [];
2334
- while (words.length) {
2335
- let take = Math.ceil(words.length / Math.ceil(words.length / 4));
2336
- const last = words[take - 1]?.replace(/^[^A-Za-zÀ-ÿ]+|[^A-Za-zÀ-ÿ]+$/g, "").toLowerCase();
2337
- if (take > 1 && connectors.has(last)) take--;
2338
- phrases.push(words.splice(0, take).join(" "));
2339
- }
2340
- return phrases;
2341
- }
2342
2334
  function carryContent(slide, to) {
2343
2335
  const target = LIST_KEY[to];
2344
2336
  if (!target) return;
2345
2337
  const activeKey = LIST_KEY[slide.layout];
2346
2338
  if (activeKey === target && Array.isArray(slide[target]) && slide[target].length) return;
2347
- if (to === "kinetic") {
2348
- const source = SOURCE_KEYS.find((key) => Array.isArray(slide[key]) && slide[key].length);
2349
- if (source) {
2350
- slide.beats = slide[source].map((item) => {
2351
- if (typeof item !== "object" || item == null) return { text: String(item ?? "") };
2352
- return { text: item.title ?? item.label ?? item.text ?? item.name ?? "", ...(item.text && item.title ? { tag: item.text } : {}) };
2353
- });
2354
- } else {
2355
- const phrases = [...kineticPhrases(slide.title || slide.text), ...kineticPhrases(slide.subtitle)];
2356
- if (phrases.length) slide.beats = phrases.map((text, i) => ({ text, ...(i === 0 && slide.kicker ? { tag: slide.kicker } : {}) }));
2357
- }
2358
- return;
2359
- }
2360
2339
  const srcKey = [activeKey, ...SOURCE_KEYS].find((k) => k && Array.isArray(slide[k]) && slide[k].length);
2361
2340
  if (!srcKey) return;
2362
2341
  const entries = slide[srcKey].map((x) => {
@@ -2387,7 +2366,7 @@
2387
2366
  }
2388
2367
  if (layoutName === "quote") slide.quote = primary;
2389
2368
  if (["question", "poll"].includes(layoutName)) slide.question = primary;
2390
- if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length && !["kinetic", "statement"].includes(layoutName)) {
2369
+ if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length && layoutName !== "statement") {
2391
2370
  const content = [slide.subtitle, slide.body, primary].find(v => typeof v === "string" && v.trim());
2392
2371
  if (content) slide[LIST_KEY[layoutName]] = content.split(/\n+/).filter(Boolean).map(text => ["list", "split", "question", "poll", "references", "end"].includes(layoutName) ? text : {title:text});
2393
2372
  }
@@ -2448,6 +2427,23 @@
2448
2427
  } catch (e) { showToast(`Não deu para atualizar: ${e.message}`); }
2449
2428
  },
2450
2429
  hydrate: hydrateIcons,
2430
+ splitCalendar: async () => {
2431
+ const at = state.currentSlideIndex, slide = state.deck.slides[at];
2432
+ const { calendarMonth, calendarMonths } = window.SagaCalendarMonths;
2433
+ const MESES = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
2434
+ const months = calendarMonths(slide);
2435
+ if (months.length < 2) return;
2436
+ const { uid, events, undated, month, week, ...base } = slide;
2437
+ const slides = months.map((m, k) => ({ ...base, ...(k === 0 && uid ? { uid } : {}), view: "mes", month: m,
2438
+ kicker: base.kicker || `${MESES[+m.slice(5) - 1]} ${m.slice(0, 4)}`,
2439
+ events: events.filter((e) => calendarMonth(e?.date) === m),
2440
+ ...(k === 0 && (undated?.length || events.some((e) => !calendarMonth(e?.date))) ? { undated: [...(undated || []), ...events.filter((e) => !calendarMonth(e?.date)).map(({ title, text }) => ({ title, ...(text ? { text } : {}) }))] } : {}) }));
2441
+ state.deck.slides.splice(at, 1, ...slides);
2442
+ await syncDeckToServer();
2443
+ renderThumbnails();
2444
+ await renderCurrentSlide();
2445
+ showToast(`Um slide por mês: ${slides.length} slides, na visão de mês.`, 4000);
2446
+ },
2451
2447
  pickIcon: (cb) => openIconPicker(null, cb),
2452
2448
  layoutLabel,
2453
2449
  generateImage: (element, btn) => {
@@ -2605,7 +2601,9 @@
2605
2601
  }
2606
2602
 
2607
2603
  async function startPresentation({ pen = false } = {}) {
2608
- await syncDeckToServer(); // a prévia é montada a partir do deck do servidor
2604
+ // a apresentação é montada a partir do deck do servidor. Modelo em prévia: o servidor já tem o mesmo deck (qualquer
2605
+ // mudança teria criado a cópia e saído da prévia), e enviar de novo contaria como mudança: apresentar não grava nada
2606
+ if (!state.preview) await syncDeckToServer();
2609
2607
  const start = state.currentSlideIndex + 1;
2610
2608
  dom.presModal.classList.remove("hidden");
2611
2609
  dom.presFrame.src = scopedURL(`preview?t=${Date.now()}#${start}`);
@@ -2844,7 +2842,7 @@
2844
2842
  // outra aba/aparelho abriu outra apresentação neste Studio: nada desta foi gravado (o servidor recusou)
2845
2843
  function staleTab(msg) { updateSaveStatus("error"); showToast(msg, 20000, { label: "Recarregar", fn: () => location.reload() }); }
2846
2844
 
2847
- async function syncDeckToServer() {
2845
+ async function syncDeckToServer({ throwOnError = false } = {}) {
2848
2846
  trackDeck("Edição");
2849
2847
  updateSaveStatus("saving");
2850
2848
  try {
@@ -2854,7 +2852,7 @@
2854
2852
  body: JSON.stringify({ spec: state.deck, expectFile: state.file }),
2855
2853
  });
2856
2854
  const data = await res.json().catch(() => ({}));
2857
- if (data.stale) return staleTab(data.error);
2855
+ if (data.stale) { staleTab(data.error); if (throwOnError) throw new Error(data.error); return; }
2858
2856
  if ("file" in data) state.file = data.file;
2859
2857
  if (data.materialized) previewMaterialized(data.materialized);
2860
2858
  // o servidor dá identidade a slide novo (uid) e leva o ajuste visual junto quando um texto muda: o Studio adota
@@ -2868,9 +2866,11 @@
2868
2866
  if (adopted && !state.applyingHistory) deckHistory.rebase(state.deck);
2869
2867
  updateSaveStatus(res.ok ? "saved" : "error");
2870
2868
  if (res.ok) window.SagaProject?.deckChanged();
2869
+ else if (throwOnError) throw new Error(data.error || 'Não foi possível salvar as mudanças antes de exportar.');
2871
2870
  } catch (err) {
2872
2871
  console.warn("Erro ao sincronizar com servidor:", err);
2873
2872
  updateSaveStatus("error");
2873
+ if (throwOnError) throw err;
2874
2874
  }
2875
2875
  }
2876
2876
 
@@ -3631,9 +3631,8 @@ ${ta.value}`;
3631
3631
 
3632
3632
  const isMobile = () => window.matchMedia("(max-width: 900px)").matches;
3633
3633
 
3634
- // Painel lateral: Formatar, Assistente ou Variáveis
3635
- // abas do painel lateral: Formatar (conteúdo), Propriedades (inspetor), Assistente, Variáveis
3636
- const PANE_TABS = ["props", "inspect", "chat", "vars"];
3634
+ // Formatar, Propriedades e Variáveis ficam agrupados; o Assistente tem acesso fixo separado, por último.
3635
+ const PANE_TABS = ["props", "inspect", "vars", "chat"];
3637
3636
  const paneEls = (w) => ({ btn: document.getElementById(`tab-btn-${w}`), panel: document.getElementById(`tab-panel-${w}`) });
3638
3637
  const currentPane = () => PANE_TABS.find((w) => paneEls(w).panel?.classList.contains("active")) || "props";
3639
3638
  // menu de botão direito genérico (itens: { label, ic, fn, danger, disabled } ou { sep: true })
@@ -3661,7 +3660,7 @@ ${ta.value}`;
3661
3660
  const fab = document.getElementById("chat-fab");
3662
3661
  if (!fab) return;
3663
3662
  const visible = isMobile() ? dom.inspectorSidebar.classList.contains("mobile-open") : !dom.inspectorSidebar.classList.contains("collapsed");
3664
- fab.classList.toggle("hidden", visible && currentPane() === "chat");
3663
+ fab.classList.toggle("hidden", visible); // com o painel aberto, o botão Assistente já está sempre à vista
3665
3664
  const badge = document.getElementById("chat-badge");
3666
3665
  if (visible && currentPane() === "chat" && badge?.classList.contains("show")) badge.classList.remove("show"); // só se precisar: o observador do selo chama esta função
3667
3666
  fab.classList.toggle("has-news", !!document.getElementById("chat-badge")?.classList.contains("show"));
@@ -4310,29 +4309,24 @@ ${ta.value}`;
4310
4309
  return { res, name };
4311
4310
  }
4312
4311
 
4313
- // PowerPoint / PDF / roteiro: levam alguns segundos (o Chrome invisível desenha cada slide)
4312
+ // PowerPoint / PDF / roteiro: acompanham a fila do servidor, sem prender a página no download.
4314
4313
  const EXPORTS = {
4315
- pptx: { label: "o PowerPoint", done: "PowerPoint editável, com animações e notas." },
4316
- pdf: { label: "o PDF", done: "PDF com um slide por página." },
4317
- roteiro: { label: "o roteiro", done: "roteiro com miniaturas, notas e tempos." },
4318
- estudo: { label: "o material de estudo", done: "material de estudo: cada slide inteiro e o texto de consulta." },
4319
- "estudo-html": { label: "o material de estudo", done: "material de estudo num HTML só." },
4320
- tudo: { label: "o PowerPoint, o PDF e o roteiro", done: "um .zip com o PowerPoint (com notas), o PDF e o roteiro." },
4314
+ pptx: "o PowerPoint", pdf: "o PDF", roteiro: "o roteiro", estudo: "o material de estudo",
4315
+ "estudo-html": "o material de estudo", tudo: "o PowerPoint, o PDF e o roteiro",
4321
4316
  };
4322
4317
  document.querySelectorAll("[data-export]").forEach((btn) => {
4323
4318
  btn.onclick = async (e) => {
4324
4319
  e.preventDefault();
4325
4320
  if (btn.disabled) return;
4326
4321
  const kind = btn.dataset.export, clean = btn.dataset.notas === "0";
4327
- const x = clean ? { label: "o PowerPoint sem as notas", done: "PowerPoint sem as notas do apresentador, pronto para mandar." } : EXPORTS[kind];
4322
+ const label = clean ? "o PowerPoint sem as notas" : EXPORTS[kind];
4328
4323
  btn.disabled = true;
4329
- showToast(`Gerando ${x.label} (${state.deck.slides.length} slides)… pode levar alguns segundos.`, 60000);
4330
4324
  try {
4331
- const { res, name } = await downloadFrom(`api/export/${kind}${clean ? "?notas=0" : ""}`, `apresentacao.${{ pptx: "pptx", tudo: "zip", "estudo-html": "html" }[kind] || "pdf"}`);
4332
- const warns = JSON.parse(decodeURIComponent(res.headers.get("X-Sagadeck-Warnings") || "%5B%5D"));
4333
- showToast(warns.length ? `"${name}" baixado, com ${warns.length} aviso(s): ${warns.slice(0, 2).join("; ")}` : `"${name}" baixado: ${x.done}`, warns.length ? 9000 : 4000);
4325
+ await window.SagadeckExports.start({ kind, notes: !clean, title: state.deck.title }, { beforeStart: async () => {
4326
+ if (!state.preview) await syncDeckToServer({ throwOnError: true });
4327
+ } });
4334
4328
  } catch (err) {
4335
- showToast(`Não deu para gerar ${x.label}: ${err.message}`, 8000);
4329
+ showToast(`Não deu para gerar ${label}: ${err.message}`, 8000);
4336
4330
  } finally {
4337
4331
  btn.disabled = false;
4338
4332
  }
@@ -15,6 +15,7 @@
15
15
  let treeData = null;
16
16
  const openTabs = []; // { path, kind, name, dirty, text, sheet }
17
17
  let active = "__deck__";
18
+ let slideLayout = "";
18
19
  const expanded = new Set(["contexto"]);
19
20
  const $ = (s, r = document) => r.querySelector(s);
20
21
  const esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -225,7 +226,8 @@
225
226
  function bindTabs() {
226
227
  $("#doc-tabs").addEventListener("click", (ev) => {
227
228
  const c = ev.target.closest("[data-close]"); if (c) { ev.stopPropagation(); closeTab(c.dataset.close); return; }
228
- const t = ev.target.closest("[data-doc]"); if (t) activate(t.dataset.doc);
229
+ // A aba ativa já tem a grade e a seleção da pessoa: reler por clique perderia ambas na resposta tardia.
230
+ const t = ev.target.closest("[data-doc]"); if (t && t.dataset.doc !== active) activate(t.dataset.doc);
229
231
  });
230
232
  $("#doc-tabs").addEventListener("auxclick", (ev) => { const t = ev.target.closest("[data-doc]"); if (ev.button === 1 && t && t.dataset.doc !== "__deck__") closeTab(t.dataset.doc); });
231
233
  }
@@ -241,16 +243,26 @@
241
243
  if (!openTabs.some((t) => t.path === p)) openTabs.push({ path: p, kind, name: p.split("/").pop() });
242
244
  await activate(p);
243
245
  }
246
+ // Com um mapa, a planilha e os pontos precisam estar à vista e receber cliques ao mesmo tempo.
247
+ function syncLayout() {
248
+ const t = openTabs.find((x) => x.path === active);
249
+ const mapSheet = t?.kind === "sheet" && slideLayout === "map";
250
+ const side = t?.kind === "deck" || mapSheet;
251
+ const editor = $("#canvas-stage-wrapper");
252
+ editor.classList.toggle("doc-side", active !== "__deck__" && side);
253
+ editor.classList.toggle("doc-full", active !== "__deck__" && !side);
254
+ if (t?.kind === "sheet") $("#doc-view .sh-body")?.classList.toggle("no-suggest", t.hideSuggest ?? mapSheet);
255
+ }
256
+ function slideChanged(slide) { slideLayout = slide?.layout || ""; syncLayout(); ctx?.relayout(); }
244
257
  async function activate(p) {
245
258
  active = p;
246
259
  renderTabs();
247
260
  $("#project-tree")?.querySelectorAll(".ex-file").forEach((r) => r.classList.toggle("active", r.dataset.path === p));
248
- const editor = $("#canvas-stage-wrapper"), view = $("#doc-view");
249
- editor.classList.remove("doc-side", "doc-full");
261
+ const view = $("#doc-view");
262
+ syncLayout();
250
263
  if (p === "__deck__") { view.innerHTML = ""; view.classList.add("hidden"); ctx.relayout(); return; }
251
264
  const t = openTabs.find((x) => x.path === p);
252
265
  view.classList.remove("hidden");
253
- editor.classList.add(t.kind === "deck" ? "doc-side" : "doc-full");
254
266
  try {
255
267
  if (t.kind === "deck") await showDeckText(view, t);
256
268
  else if (t.kind === "md" || t.kind === "text" || (t.kind === "sheet" && t.asText)) await showText(view, t);
@@ -349,13 +361,14 @@
349
361
  const sepInfo = data.delimiter ? ` · separado por ${DELIM[data.delimiter] || data.delimiter}` : "";
350
362
  const asText = /\.(csv|tsv)$/i.test(t.name) ? `<button type="button" class="btn btn-small" data-as-text title="Ver o arquivo como texto"><i class="ic" data-ic="file-code"></i> Texto</button>` : "";
351
363
  view.innerHTML = `<div class="dv-head"><i class="ic" data-ic="file-spreadsheet"></i><b>${esc(t.name)}</b><span class="dv-status" id="dv-status">${sheet.total} linhas · ${cols.length} colunas${sepInfo}${editable ? "" : " · só leitura"}</span>${asText}<button type="button" class="btn btn-small" data-toggle-suggest title="Gráficos que fazem sentido com estes dados"><i class="ic" data-ic="chart-column"></i> Gráficos</button></div>${tabs}
352
- <div class="sh-body${t.hideSuggest ? " no-suggest" : ""}"><div class="sh-grid-wrap" id="sh-grid"></div>
364
+ <div class="sh-body${(t.hideSuggest ?? (slideLayout === "map")) ? " no-suggest" : ""}"><div class="sh-grid-wrap" id="sh-grid"></div>
353
365
  <aside class="sh-suggest"><div class="sh-suggest-head"><b>Gráficos que fazem sentido</b><button type="button" class="btn btn-small" data-ai-suggest title="A IA confere o tipo das colunas e sugere gráficos, títulos e nomes dos eixos"><i class="ic" data-ic="sparkles"></i> Sugerir com IA</button></div><div class="sh-cards" id="sh-cards"><div class="ex-empty">Olhando as colunas…</div></div></aside></div>`;
354
366
  const types = [];
355
367
  for (const c of cols) types[c.index] = { type: c.type, label: TYPE_LABEL[c.type] || c.type };
356
368
  let timer = 0;
357
- window.SagaViewers.sheetGrid(view.querySelector("#sh-grid"), {
369
+ const grid = window.SagaViewers.sheetGrid(view.querySelector("#sh-grid"), {
358
370
  rows: sheet.rows, editable, hasHeader: sheet.hasHeader, types,
371
+ onSelect: (r) => window.SagaMapData?.selectRow(t.path, r + 1),
359
372
  onChange: (rows) => {
360
373
  const st = $("#dv-status"); st.textContent = "gravando…"; t.dirty = true; renderTabs();
361
374
  clearTimeout(timer);
@@ -365,9 +378,11 @@
365
378
  }, 500);
366
379
  },
367
380
  });
381
+ // planilha com posição ou endereço: "Pôr no mapa" e "Achar coordenadas" (map-data.js)
382
+ window.SagaMapData?.sheetTools(view, { t, rows: sheet.rows, grid, api, ctx, reload: async (p) => { if (p && p !== t.path) return; t.text = undefined; await activate(t.path); } });
368
383
  view.querySelectorAll("[data-sheet]").forEach((b) => b.onclick = () => { t.sheet = b.dataset.sheet; activate(t.path); });
369
384
  view.querySelector("[data-as-text]")?.addEventListener("click", () => { t.asText = true; t.text = undefined; activate(t.path); });
370
- view.querySelector("[data-toggle-suggest]").onclick = () => { t.hideSuggest = !t.hideSuggest; view.querySelector(".sh-body").classList.toggle("no-suggest", t.hideSuggest); };
385
+ view.querySelector("[data-toggle-suggest]").onclick = () => { t.hideSuggest = !view.querySelector(".sh-body").classList.contains("no-suggest"); view.querySelector(".sh-body").classList.toggle("no-suggest", t.hideSuggest); };
371
386
  view.querySelector("[data-ai-suggest]").onclick = (ev) => loadSuggestions(t, true, ev.currentTarget);
372
387
  ctx.hydrate(view);
373
388
  loadSuggestions(t, false);
@@ -439,5 +454,5 @@
439
454
  // o deck mudou por outro caminho (formulário, IA, arrastar): a aba do texto da apresentação acompanha
440
455
  function deckChanged() { openTabs.find((t) => t.kind === "deck")?.refresh?.(); }
441
456
  const wantsPaste = () => !$("#project-explorer").classList.contains("hidden") || active !== "__deck__";
442
- window.SagaProject = { setup, refreshTree, openFile, deckChanged, savePastedImage, uploadFile, wantsPaste, activeTab: () => active };
457
+ window.SagaProject = { setup, refreshTree, openFile, deckChanged, slideChanged, savePastedImage, uploadFile, wantsPaste, activeTab: () => active };
443
458
  })();