sagadeck 1.7.1 → 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.
- package/README.md +40 -0
- package/docs/ANDAMENTO.md +122 -0
- package/docs/REFERENCIA.md +58 -0
- package/package.json +2 -1
- package/src/ai/deck-ai.js +13 -4
- package/src/ai/map-analysis-run.js +119 -0
- package/src/ai/map-data.js +221 -0
- package/src/build.js +6 -2
- package/src/export/browser.js +23 -1
- package/src/export/pptx.js +2 -1
- package/src/export/shots.js +3 -1
- package/src/layouts.js +10 -0
- package/src/map-analysis.js +106 -0
- package/src/map-config.js +83 -0
- package/src/map-routing.js +82 -0
- package/src/map-services.js +60 -0
- package/src/map.js +261 -0
- package/src/runtime/base.css +20 -0
- package/src/runtime/map.js +234 -0
- package/src/runtime/runtime.js +1 -1
- package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
- package/src/runtime/vendor/geo-contornos.json +1 -0
- package/src/runtime/vendor/leaflet.css +661 -0
- package/src/runtime/vendor/leaflet.js +6 -0
- package/src/studio/ai-chat-route.js +20 -0
- package/src/studio/demo-decks.js +3 -1
- package/src/studio/geo-routes.js +138 -0
- package/src/studio/layout-samples.js +71 -0
- package/src/studio/project.js +1 -1
- package/src/studio/public/ai-settings.js +4 -1
- package/src/studio/public/app.js +7 -3
- package/src/studio/public/explorer.js +23 -8
- package/src/studio/public/index.html +4 -0
- package/src/studio/public/library.html +2 -0
- package/src/studio/public/library.js +6 -1
- package/src/studio/public/map-data.js +154 -0
- package/src/studio/public/map-editor.js +287 -0
- package/src/studio/public/map-fields.js +21 -0
- package/src/studio/public/map-settings.js +84 -0
- package/src/studio/public/slide-form.js +1 -1
- package/src/studio/public/style.css +13 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/public/viewers.js +2 -1
- package/src/studio/public/visual-editor.js +1 -1
- package/src/studio/server.js +10 -0
- package/src/studio/tour.js +76 -0
|
@@ -48,6 +48,7 @@ export const LAYOUT_INFO = {
|
|
|
48
48
|
timeline: ["Linha do tempo", "Eventos em sequência"],
|
|
49
49
|
chart: ["Gráfico de dados", "Barras, colunas, linhas, rosca: cole a tabela do Excel ou digite os valores"],
|
|
50
50
|
table: ["Tabela", "Tabela de verdade nas cores do tema: cole do Excel, destaque linha e total"],
|
|
51
|
+
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
52
|
compare: ["Comparação", "A × B lado a lado"],
|
|
52
53
|
matrix: ["Matriz 2×2", "Quatro quadrantes"],
|
|
53
54
|
question: ["Pergunta", "Pergunta para a plateia com timer"],
|
|
@@ -65,7 +66,77 @@ export const LAYOUT_INFO = {
|
|
|
65
66
|
end: ["Encerramento", "Obrigado e contatos"],
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
// Como pedir cada layout à IA, em português comum: aparece no "Conheça o SagaDeck" (src/studio/tour.js), para quem vê
|
|
70
|
+
// o recurso saber a frase que o gera. Exemplos de áreas variadas, nunca de um setor só.
|
|
71
|
+
export const LAYOUT_ASK = {
|
|
72
|
+
portal: "Abra a palestra entrando numa porta que transforma o cenário e revela o tema",
|
|
73
|
+
playground: "Faça um slide interativo em que a plateia mexe num controle e vê a cor do céu mudar",
|
|
74
|
+
codelab: "Monte um exercício de código em que os alunos alteram a entrada e executam passo a passo",
|
|
75
|
+
graphlab: "Mostre a rede de metrô da cidade e deixe testar a rota mais curta tirando uma estação",
|
|
76
|
+
mosaic: "Liste as seis frentes do projeto num mosaico que se ajusta sozinho",
|
|
77
|
+
ribbon: "Mostre as três fases da obra em cápsulas, uma ao lado da outra",
|
|
78
|
+
dossier: "Faça uma página de consulta com os comandos e as respostas da API, para guardar",
|
|
79
|
+
decisionlab: "Compare revisão humana e automação com premissas que eu possa ajustar ao vivo",
|
|
80
|
+
solution: "Resolva passo a passo quanto rende uma aplicação de R$ 1.000 a 1% ao mês em três anos",
|
|
81
|
+
calc: "Faça uma calculadora do consumo de energia de uma casa, com faixas de baixo, médio e alto",
|
|
82
|
+
algo: "Mostre a busca binária rodando de verdade, com o código e as variáveis",
|
|
83
|
+
carousel: "Apresente cinco destinos de viagem, um por clique, com foto e uma frase",
|
|
84
|
+
duel: "Simule duas pessoas editando o mesmo arquivo no Git até dar conflito",
|
|
85
|
+
terminals: "Mostre dois terminais lado a lado: um faz push e o outro puxa a mudança",
|
|
86
|
+
turns: "Encene um diálogo entre cliente e atendente, uma fala por clique",
|
|
87
|
+
science: "Mostre a parábola y = ax² + bx + c com controles para a, b e c",
|
|
88
|
+
infographic: "Faça um infográfico com os cinco pilares da sustentabilidade em volta de um centro",
|
|
89
|
+
diagram: "Desenhe o fluxo de atendimento de um pedido, da chegada à entrega",
|
|
90
|
+
scenography: "Abra com a frase \"Além do óbvio\" num cenário de palco",
|
|
91
|
+
cover: "Faça a capa da palestra sobre energia solar, com o meu nome e a data",
|
|
92
|
+
section: "Abra a segunda parte, sobre os resultados",
|
|
93
|
+
statement: "Uma frase de impacto: a água mais barata é a que não se desperdiça",
|
|
94
|
+
calendar: "Mostre num calendário as datas das oficinas do semestre",
|
|
95
|
+
poster: "Faça um pôster de uma página explicando como funciona uma placa solar",
|
|
96
|
+
definition: "Defina \"sustentabilidade\", com a origem da palavra",
|
|
97
|
+
headline: "Uma manchete enorme: 40% da água tratada se perde no caminho",
|
|
98
|
+
quote: "Ponha a citação de Paulo Freire sobre ensinar, com o autor",
|
|
99
|
+
onepage: "Resuma o projeto numa página: jornada, problema com números, solução e painel",
|
|
100
|
+
status: "Faça o status da semana: feito, em andamento, bloqueios e próximos passos",
|
|
101
|
+
hub: "Monte um menu em que a plateia escolhe o caminho: iniciante, intermediário ou avançado",
|
|
102
|
+
number: "Mostre o número 1,2 milhão de visitantes, contando até ele",
|
|
103
|
+
split: "Explique o ciclo da água com o texto de um lado e uma ilustração do outro",
|
|
104
|
+
full: "Uma foto de floresta ocupando o slide inteiro",
|
|
105
|
+
cards: "Três cartões com os benefícios do programa, cada um com ícone",
|
|
106
|
+
bento: "Monte um mosaico de blocos com os destaques do ano",
|
|
107
|
+
stats: "Mostre quatro indicadores do trimestre com a tendência de cada um",
|
|
108
|
+
steps: "Mostre em etapas como instalar um sensor de umidade",
|
|
109
|
+
funnel: "Faça o funil de inscrições: visitantes, cadastros, inscritos e concluintes",
|
|
110
|
+
pyramid: "Monte a pirâmide de prioridades, do essencial ao desejável",
|
|
111
|
+
list: "Liste os cinco cuidados antes de uma viagem longa",
|
|
112
|
+
agenda: "Faça a agenda do evento com a parte atual destacada",
|
|
113
|
+
timeline: "Faça a linha do tempo da história do cinema brasileiro",
|
|
114
|
+
chart: "Faça um gráfico de linhas com a temperatura média de cada mês",
|
|
115
|
+
table: "Monte a tabela comparando três planos, com o total destacado",
|
|
116
|
+
map: "Mostre no mapa as estações de medição da planilha, coloridas pelo status, com um cartão ao clicar",
|
|
117
|
+
compare: "Compare transporte público e carro próprio lado a lado",
|
|
118
|
+
matrix: "Monte uma matriz 2x2 de esforço e impacto com as ideias do time",
|
|
119
|
+
question: "Faça uma pergunta para a plateia com cronômetro de 30 segundos",
|
|
120
|
+
poll: "Faça uma enquete ao vivo: qual tema vocês querem aprofundar?",
|
|
121
|
+
image: "Ponha esta foto em tela cheia com uma legenda curta",
|
|
122
|
+
code: "Mostre este código Python com as linhas da conexão destacadas",
|
|
123
|
+
codewalk: "Explique este código uma linha por vez, com a saída simulada",
|
|
124
|
+
api: "Faça uma chamada de API ao vivo que consulta o clima e mostra a resposta",
|
|
125
|
+
spotlight: "Conduza o olhar pelas partes desta planta baixa, uma por vez",
|
|
126
|
+
kinetic: "Faça a abertura com frases curtas aparecendo em ritmo, estilo tipografia cinética",
|
|
127
|
+
video: "Ponha este vídeo do YouTube no slide",
|
|
128
|
+
blocks: "Monte livremente um slide com título, dois textos e uma imagem em colunas",
|
|
129
|
+
canvas: "Posicione o logotipo, o título e a foto onde eu indicar",
|
|
130
|
+
references: "Liste as referências bibliográficas no fim",
|
|
131
|
+
end: "Feche com um obrigado e os meus contatos",
|
|
132
|
+
};
|
|
133
|
+
|
|
68
134
|
export const LAYOUT_SAMPLES = {
|
|
135
|
+
map: { layout: "map", kicker: "Monitoramento", title: "Estações de ==qualidade do ar==", basemap: "claro", layers: [
|
|
136
|
+
{ name: "Estações", color: "status", label: "nome", points: [
|
|
137
|
+
{ nome: "Centro", lat: -23.5489, lon: -46.6388, status: "Boa", pm25: 12 }, { nome: "Pinheiros", lat: -23.5614, lon: -46.7020, status: "Moderada", pm25: 24 },
|
|
138
|
+
{ nome: "Ibirapuera", lat: -23.5874, lon: -46.6576, status: "Boa", pm25: 9 }, { nome: "Mooca", lat: -23.5596, lon: -46.5996, status: "Ruim", pm25: 41 },
|
|
139
|
+
{ nome: "Santana", lat: -23.5027, lon: -46.6252, status: "Moderada", pm25: 27 }], colors: { Ruim: "alert" } }] },
|
|
69
140
|
portal: portalExample,
|
|
70
141
|
playground: playgroundExample,
|
|
71
142
|
codelab: codeLabExample,
|
package/src/studio/project.js
CHANGED
|
@@ -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();
|
|
@@ -13,8 +13,10 @@ window.SagaAISettings = (() => {
|
|
|
13
13
|
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
-
|
|
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();
|
package/src/studio/public/app.js
CHANGED
|
@@ -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", "scenography", "portal", "science", "kinetic", "video", "carousel", "duel", "terminals", "turns", "solution", "calc", "algo", "graphlab", "codelab", "playground",
|
|
39
39
|
"blocks", "dossier", "canvas", "references", "end",
|
|
40
40
|
];
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@
|
|
|
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
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",
|
|
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",
|
|
@@ -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 });
|
|
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,7 +2049,7 @@
|
|
|
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"]],
|
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -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
|
-
|
|
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
|
|
249
|
-
|
|
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 = !
|
|
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
|
})();
|
|
@@ -849,6 +849,9 @@
|
|
|
849
849
|
<script src="ai-settings.js"></script>
|
|
850
850
|
<script src="api-envs-form.js"></script>
|
|
851
851
|
<script src="calc-fields.js"></script>
|
|
852
|
+
<script src="map-fields.js"></script>
|
|
853
|
+
<script src="map-settings.js"></script>
|
|
854
|
+
<script src="map-data.js"></script>
|
|
852
855
|
<script src="art-preview.js"></script>
|
|
853
856
|
<script src="api-collections.js"></script>
|
|
854
857
|
<script src="developer-fields.js"></script>
|
|
@@ -858,6 +861,7 @@
|
|
|
858
861
|
<script src="formula.js"></script>
|
|
859
862
|
<script src="calc.js"></script>
|
|
860
863
|
<script src="science.js"></script>
|
|
864
|
+
<script src="map.js"></script>
|
|
861
865
|
<script src="diagram.js"></script>
|
|
862
866
|
<script src="decision-lab.js"></script>
|
|
863
867
|
<script src="visual-editor.js"></script>
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<aside class="lib-side">
|
|
34
34
|
<nav id="side" aria-label="Biblioteca"></nav>
|
|
35
35
|
<button type="button" class="lib-btn ghost ai-settings" id="btn-ai" hidden title="Provedores, chaves e modelos de IA"><i class="ic" data-ic="sliders-horizontal"></i><span class="lbl">Configurar IA</span></button>
|
|
36
|
+
<button type="button" class="lib-btn ghost ai-settings" id="btn-map" hidden title="Serviços do mapa: fundo, endereços e rotas (opcional)"><i class="ic" data-ic="map"></i><span class="lbl">Configurar mapa</span></button>
|
|
36
37
|
</aside>
|
|
37
38
|
<main class="lib-main" id="main" tabindex="-1" aria-live="polite"><div class="initial-loading"><span class="loading-orbit"></span>Preparando seu estúdio…</div></main>
|
|
38
39
|
</div>
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
<script src="ui-icons.js"></script>
|
|
54
55
|
<script src="update-notice.js"></script>
|
|
55
56
|
<script src="ai-settings.js"></script>
|
|
57
|
+
<script src="map-settings.js"></script>
|
|
56
58
|
<script src="library.js"></script>
|
|
57
59
|
</body>
|
|
58
60
|
</html>
|
|
@@ -62,11 +62,12 @@
|
|
|
62
62
|
// e, enquanto a IA não estiver configurada, a faixa "Configure a IA" (some ao salvar)
|
|
63
63
|
const aiSaved = () => { $("#ai-banner").hidden = true; };
|
|
64
64
|
api("api/ia").then(({ editable, configured, problem }) => {
|
|
65
|
-
$("#btn-ai").hidden = !editable; $("#ai-banner").hidden = !editable || configured;
|
|
65
|
+
$("#btn-ai").hidden = !editable; $("#btn-map").hidden = !editable; $("#ai-banner").hidden = !editable || configured;
|
|
66
66
|
if (problem) $("#ai-banner span").textContent = `A IA não está configurada. ${problem}`; // o arquivo existe mas não serve
|
|
67
67
|
hydrate($("#ai-banner"));
|
|
68
68
|
}).catch(() => {});
|
|
69
69
|
$("#btn-ai").onclick = () => window.SagaAISettings.open(aiSaved);
|
|
70
|
+
$("#btn-map").onclick = () => window.SagaMapSettings.open(); // opcional: o mapa funciona sem configurar nada
|
|
70
71
|
$("#ai-banner-btn").onclick = () => window.SagaAISettings.open(aiSaved);
|
|
71
72
|
const openEditor = (id, present) => { location.href = `editor?deck=${encodeURIComponent(id)}${present ? "&present=1" : ""}`; };
|
|
72
73
|
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
$("#side").innerHTML = `
|
|
77
78
|
<button class="nav ${view === "recentes" ? "active" : ""}" data-view="recentes">${ic("clock")}<span class="name">Recentes</span></button>
|
|
78
79
|
<button class="nav ${view === "todas" ? "active" : ""}" data-view="todas">${ic("layout-grid")}<span class="name">Todas</span><span class="count">${data.decks.length || ""}</span></button>
|
|
80
|
+
<button class="nav nav-tour" data-tour title="Tudo o que dá para fazer, um slide por recurso (abre em prévia)">${ic("compass")}<span class="name">Conheça o SagaDeck</span></button>
|
|
79
81
|
<div class="side-title">TÓPICOS<button title="Novo tópico" id="new-topic" aria-label="Novo tópico">${ic("plus")}</button></div>
|
|
80
82
|
${data.topics.map((t) => `<button class="nav ${view === t.id ? "active" : ""}" data-view="${esc(t.id)}" data-topic="${esc(t.id)}"><span class="dot" style="background:${esc(t.color)}"></span><span class="name">${esc(t.name)}</span><span class="count">${t.count || ""}</span></button>`).join("")}
|
|
81
83
|
${loose ? `<button class="nav ${view === "" ? "active" : ""}" data-view="" title="Arquivos .yaml deixados direto na pasta da biblioteca"><span class="dot" style="background:${NO_TOPIC.color}"></span><span class="name">${NO_TOPIC.name}</span><span class="count">${loose}</span></button>` : ""}
|
|
@@ -86,6 +88,8 @@
|
|
|
86
88
|
hydrate($("#side"));
|
|
87
89
|
$("#side").querySelectorAll("[data-view]").forEach((b) => b.onclick = () => { view = b.dataset.view; store.set("libView", view); render(); });
|
|
88
90
|
$("#new-topic").onclick = () => topicDialog();
|
|
91
|
+
// a apresentação de recursos (src/studio/tour.js) abre como os modelos da vitrine: em prévia, sem criar arquivo
|
|
92
|
+
$("#side [data-tour]").onclick = () => { location.href = "editor?model=conheca&topic=Modelos"; };
|
|
89
93
|
$("#side").querySelectorAll("[data-topic]").forEach((b) => {
|
|
90
94
|
b.ondragover = (e) => { if (e.dataTransfer.types.includes("application/x-sagadeck-deck")) { e.preventDefault(); b.classList.add("drop"); } };
|
|
91
95
|
b.ondragleave = () => b.classList.remove("drop");
|
|
@@ -277,6 +281,7 @@
|
|
|
277
281
|
["visual", "model-lavanda", "Estúdio lavanda", "Mosaicos, cápsulas e composições editoriais", ["#EFE9FB", "#5B3FA8", "#D8C8F5"]],
|
|
278
282
|
["visual", "model-executivo", "Relatório executivo", "Resumo, indicadores e plano de trabalho", ["#0F1115", "#E4B660", "#EDEBE6"]],
|
|
279
283
|
["visual", "model-workshop", "Workshop visual", "Perguntas, código guiado e consulta", ["#FFF7E6", "#1A1A1A", "#E8590C"]],
|
|
284
|
+
["recurso", "model-conheca", "Conheça o SagaDeck", "Um slide por recurso, com a frase para pedir cada um à IA", ["#F2F0EA", "#1A1A1A", "#F2B33D"]],
|
|
280
285
|
["recurso", "model-explorar", "Explore uma ideia", "Prever, comparar cenários, curvas conectadas e acompanhar o mesmo objeto entre cenas", ["#F4F1EA", "#1B1F3B", "#E8590C"]],
|
|
281
286
|
["recurso", "model-novidades", "Novidades", "Carrossel, gráfico do Excel e da planilha do projeto, fórmulas vivas, duelo de commits, terminais e zoom", ["#F4F1EA", "#1B1F3B", "#E8590C"]],
|
|
282
287
|
["recurso", "model-hidraulica", "Aula de hidráulica", "Navier–Stokes, curva que se mexe, calculadora de Reynolds ao vivo e exercício resolvido passo a passo", ["#EAF4F8", "#0B5C7A", "#1B998B"]],
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Planilha × mapa no Studio (aba Arquivos, planilha aberta): "Pôr no mapa" cria o slide de mapa a partir dela,
|
|
2
|
+
// "Achar coordenadas" preenche latitude e longitude pelo endereço (pergunta antes de mandar os endereços para o
|
|
3
|
+
// serviço de fora; mostra o progresso; o que não achou fica listado), e a seleção anda junto: clicar num ponto do
|
|
4
|
+
// mapa seleciona a linha da planilha aberta, e selecionar a linha abre o cartão do ponto.
|
|
5
|
+
window.SagaMapData = (() => {
|
|
6
|
+
const norm = (s) => String(s ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
7
|
+
const LAT = ["lat", "latitude"], LON = ["lon", "lng", "long", "longitude"], COORD = ["coordenadas", "coordenada", "coords", "latlon", "latlong", "posicao", "localizacao"];
|
|
8
|
+
const ADDR = ["endereco", "address", "logradouro", "rua", "enderecocompleto"], NAME = ["nome", "name", "titulo", "local", "estacao", "unidade", "ponto", "descricao"];
|
|
9
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
10
|
+
const find = (head, names) => head.findIndex((h) => names.includes(norm(h)));
|
|
11
|
+
|
|
12
|
+
// o que a planilha tem: posição, endereço, uma coluna boa para cor (2 a 8 categorias) e uma para o nome
|
|
13
|
+
function guess(rows) {
|
|
14
|
+
const head = (rows[0] || []).map((h) => String(h).trim()), body = rows.slice(1).filter((r) => r.some((c) => String(c).trim()));
|
|
15
|
+
const lat = find(head, LAT), lon = find(head, LON), coord = find(head, COORD), addr = find(head, ADDR);
|
|
16
|
+
const pos = new Set([lat, lon, coord, addr]);
|
|
17
|
+
const distinct = (i) => new Set(body.map((r) => String(r[i] ?? "").trim()).filter(Boolean)).size;
|
|
18
|
+
const isNum = (i) => body.length && body.every((r) => !String(r[i] ?? "").trim() || /^[-+]?\d+([.,]\d+)?$/.test(String(r[i]).trim()));
|
|
19
|
+
// o nome primeiro (coluna "nome"… ou a de texto com valores quase todos diferentes); a cor vai para uma coluna
|
|
20
|
+
// de categorias de verdade: valores que se repetem, de 2 a 8
|
|
21
|
+
let label = find(head, NAME);
|
|
22
|
+
if (label < 0) label = head.findIndex((h, i) => !pos.has(i) && !isNum(i) && distinct(i) > Math.min(8, body.length / 2));
|
|
23
|
+
const color = head.findIndex((h, i) => !pos.has(i) && i !== label && !isNum(i) && distinct(i) >= 2 && distinct(i) <= 8 && distinct(i) < body.length);
|
|
24
|
+
const withPos = lat >= 0 && lon >= 0 ? body.filter((r) => String(r[lat] ?? "").trim() && String(r[lon] ?? "").trim()).length : coord >= 0 ? body.filter((r) => String(r[coord] ?? "").trim()).length : 0;
|
|
25
|
+
return { head, lat, lon, coord, addr, color, label, total: body.length, withPos };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dialog(html) {
|
|
29
|
+
const d = document.createElement("dialog");
|
|
30
|
+
d.className = "md-dialog";
|
|
31
|
+
d.innerHTML = html;
|
|
32
|
+
document.body.append(d);
|
|
33
|
+
d.addEventListener("close", () => d.remove());
|
|
34
|
+
d.showModal();
|
|
35
|
+
return d;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function geocode(t, g, { api, ctx, status, reload }) {
|
|
39
|
+
const call = (extra) => fetch("api/mapa/geocode", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: t.path, ...extra }) });
|
|
40
|
+
const plano = await call({ planejar: true });
|
|
41
|
+
if (!plano.ok) { const e = await plano.json().catch(() => ({})); ctx.toast(e.error || `Não deu (HTTP ${plano.status}).`, 6000); return; }
|
|
42
|
+
const p = await plano.json();
|
|
43
|
+
let confirmo = false;
|
|
44
|
+
if (p.precisaConfirmar) {
|
|
45
|
+
const d = dialog(`<h3>Achar as coordenadas pelo endereço</h3>
|
|
46
|
+
<p>Vou enviar <b>${p.enviar} endereço${p.enviar === 1 ? "" : "s"}</b> desta planilha (coluna <b>${esc(p.coluna)}</b>) ao serviço <b>${esc(p.servico)}</b> para achar latitude e longitude. Os endereços saem desta máquina.</p>
|
|
47
|
+
<p>Leva cerca de ${p.segundos <= 1 ? "1 segundo" : p.segundos < 90 ? `${p.segundos} segundos` : `${Math.ceil(p.segundos / 60)} minutos`}, no ritmo que o serviço aceita.${p.cache ? ` ${p.cache} já estavam guardados e não serão enviados de novo.` : ""}</p>
|
|
48
|
+
<div class="md-row"><button type="button" class="btn" data-no>Cancelar</button><button type="button" class="btn btn-primary" data-yes>Enviar e achar</button></div>`);
|
|
49
|
+
const ok = await new Promise((r) => { d.querySelector("[data-yes]").onclick = () => { d.close(); r(true); }; d.querySelector("[data-no]").onclick = () => { d.close(); r(false); }; });
|
|
50
|
+
if (!ok) return;
|
|
51
|
+
confirmo = true;
|
|
52
|
+
}
|
|
53
|
+
const res = await call({ confirmo });
|
|
54
|
+
if (!res.ok) { const e = await res.json().catch(() => ({})); ctx.toast(e.error || `Não deu (HTTP ${res.status}).`, 6000); return; }
|
|
55
|
+
// progresso linha a linha (ndjson)
|
|
56
|
+
const reader = res.body.getReader(), dec = new TextDecoder();
|
|
57
|
+
let buf = "", end = null;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const { value, done } = await reader.read();
|
|
60
|
+
if (done) break;
|
|
61
|
+
buf += dec.decode(value, { stream: true });
|
|
62
|
+
let i;
|
|
63
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
64
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
65
|
+
if (!line.trim()) continue;
|
|
66
|
+
const ev = JSON.parse(line);
|
|
67
|
+
if (ev.type === "plano") status.textContent = `procurando ${ev.total} endereço${ev.total === 1 ? "" : "s"}…`;
|
|
68
|
+
if (ev.type === "progresso") status.textContent = `${ev.feitos} de ${ev.total} · ${ev.ok} achado${ev.ok === 1 ? "" : "s"}`;
|
|
69
|
+
if (ev.type === "fim") end = ev;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!end) return;
|
|
73
|
+
const msg = end.bloqueado ? `O serviço parou de responder: ${end.bloqueado}. Não tentei de novo; o que já tinha achado foi gravado.` : `${end.ok} achado${end.ok === 1 ? "" : "s"}${end.falhas.length ? `, ${end.falhas.length} sem resultado` : ""}.`;
|
|
74
|
+
if (end.falhas.length || end.bloqueado) {
|
|
75
|
+
const d = dialog(`<h3>Coordenadas pelo endereço</h3><p>${esc(msg)}</p>${end.falhas.length ? `<p>Sem resultado (corrija o endereço e tente de novo):</p><ul class="md-list">${end.falhas.slice(0, 50).map((f) => `<li>linha ${f.linha}: ${esc(f.endereco)}</li>`).join("")}</ul>` : ""}<div class="md-row"><button type="button" class="btn btn-primary" data-ok>Fechar</button></div>`);
|
|
76
|
+
d.querySelector("[data-ok]").onclick = () => d.close();
|
|
77
|
+
} else ctx.toast(msg, 5000);
|
|
78
|
+
if (end.path !== t.path) ctx.toast(`Gravei as coordenadas em ${end.path} (o xlsx não se edita aqui).`, 6000);
|
|
79
|
+
reload(end.path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// botões na cabeça da planilha aberta (explorer.js chama ao mostrar uma planilha)
|
|
83
|
+
function sheetTools(view, { t, rows, grid, api, ctx, reload }) {
|
|
84
|
+
const g = guess(rows);
|
|
85
|
+
current = { path: t.path, grid, g };
|
|
86
|
+
if (g.lat < 0 && g.coord < 0 && g.addr < 0) return;
|
|
87
|
+
const head = view.querySelector(".dv-head");
|
|
88
|
+
const bar = document.createElement("span");
|
|
89
|
+
bar.className = "md-tools";
|
|
90
|
+
const canMap = (g.lat >= 0 && g.lon >= 0) || g.coord >= 0;
|
|
91
|
+
bar.innerHTML = `${canMap ? `<button type="button" class="btn btn-small" data-to-map title="Cria um slide de mapa com os pontos desta planilha"><i class="ic" data-ic="map-pin"></i> Pôr no mapa</button>` : ""}${g.addr >= 0 && g.withPos < g.total ? `<button type="button" class="btn btn-small" data-geocode title="Preenche latitude e longitude pelo endereço"><i class="ic" data-ic="search"></i> Achar coordenadas</button>` : ""}<span class="md-status f-label"></span>`;
|
|
92
|
+
head.append(bar);
|
|
93
|
+
ctx.hydrate?.(bar);
|
|
94
|
+
const status = bar.querySelector(".md-status");
|
|
95
|
+
bar.querySelector("[data-to-map]")?.addEventListener("click", async () => {
|
|
96
|
+
const name = t.name.replace(/\.[a-z0-9]+$/i, "");
|
|
97
|
+
// o mapa lê CSV: uma planilha .xlsx ganha um CSV ao lado (o original fica como está)
|
|
98
|
+
let points = t.path;
|
|
99
|
+
if (!/\.(csv|tsv)$/i.test(t.path)) {
|
|
100
|
+
points = t.path.replace(/\.[^.]+$/, "") + ".csv";
|
|
101
|
+
try { await api("api/project/sheet-write", { path: points, rows }); } catch (e) { ctx.toast(`Não consegui gravar ${points}: ${e.message}`, 6000); return; }
|
|
102
|
+
}
|
|
103
|
+
const layer = { name, points, ...(g.color >= 0 ? { color: g.head[g.color] } : {}), ...(g.label >= 0 ? { label: g.head[g.label] } : {}) };
|
|
104
|
+
ctx.insertSlide({ layout: "map", title: name, layers: [layer] });
|
|
105
|
+
ctx.toast(points === t.path ? "Mapa inserido depois do slide atual, ligado à planilha (mudou a planilha, o mapa acompanha)." : `Mapa inserido, ligado a ${points} (o mapa lê CSV; gravei a cópia ao lado da planilha).`, 6000);
|
|
106
|
+
});
|
|
107
|
+
const geoBtn = bar.querySelector("[data-geocode]");
|
|
108
|
+
geoBtn?.addEventListener("click", async () => {
|
|
109
|
+
geoBtn.disabled = true;
|
|
110
|
+
try { await geocode(t, g, { api, ctx, status, reload }); } finally { geoBtn.disabled = false; }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// seleção ligada: ponto clicado no mapa -> linha da planilha aberta (se for a mesma planilha)
|
|
115
|
+
let current = null;
|
|
116
|
+
document.addEventListener("sagamap:ponto", (e) => {
|
|
117
|
+
const { path, linha } = e.detail || {};
|
|
118
|
+
if (!current || !linha || path !== current.path) return;
|
|
119
|
+
current.grid?.select(linha - 1, 0);
|
|
120
|
+
});
|
|
121
|
+
// linha selecionada na planilha -> cartão do ponto no mapa da prévia
|
|
122
|
+
const selectRow = (path, linha) => document.dispatchEvent(new CustomEvent("sagamap:linha", { detail: { path, linha } }));
|
|
123
|
+
|
|
124
|
+
// botão "Editar no mapa" na prévia grande; o editor (map-editor.js) só carrega no primeiro clique
|
|
125
|
+
let editorLoad;
|
|
126
|
+
const loadEditor = () => (window.SagaMapEditor ? Promise.resolve() : (editorLoad ||= new Promise((res, rej) => {
|
|
127
|
+
const s = document.createElement("script"); s.src = "map-editor.js"; s.onload = res;
|
|
128
|
+
s.onerror = () => { editorLoad = null; s.remove(); rej(new Error("Não foi possível carregar o editor do mapa.")); };
|
|
129
|
+
document.head.append(s);
|
|
130
|
+
})));
|
|
131
|
+
function attach(container, slide, commit, ctx) {
|
|
132
|
+
const E = window.SagaMapEditor;
|
|
133
|
+
if (slide?.layout !== "map") { if (E?.active()) E.stop(); document.querySelectorAll("#canvas-viewport > .me-open").forEach((b) => b.remove()); return; }
|
|
134
|
+
const box = container.querySelector(".map-box");
|
|
135
|
+
if (!box) return;
|
|
136
|
+
// o botão e a barra ficam fora do slide escalado (a prévia do Studio fica a ~40%): no #canvas-viewport
|
|
137
|
+
const host = document.getElementById("canvas-viewport") || box;
|
|
138
|
+
host.querySelectorAll(":scope > .me-open").forEach((b) => b.remove());
|
|
139
|
+
const btn = document.createElement("button");
|
|
140
|
+
btn.type = "button"; btn.className = "me-open"; btn.innerHTML = '<i class="ic" data-ic="pencil"></i> Editar no mapa';
|
|
141
|
+
for (const ev of ["pointerdown", "mousedown", "click", "dblclick"]) btn.addEventListener(ev, (e) => e.stopPropagation());
|
|
142
|
+
btn.onclick = async () => {
|
|
143
|
+
try { await loadEditor(); btn.hidden = true; window.SagaMapEditor.start(box, slide, commit, ctx); }
|
|
144
|
+
catch (e) { ctx.toast(e.message, 5000); }
|
|
145
|
+
};
|
|
146
|
+
host.append(btn);
|
|
147
|
+
ctx.hydrate?.(btn);
|
|
148
|
+
// o slide redesenhou com o editor aberto (gravou algo): volta no mesmo modo
|
|
149
|
+
if (E?.active()) { if (E.key() === (slide.uid || slide.title)) { btn.hidden = true; E.resume(box, slide, commit, ctx); } else E.stop(); }
|
|
150
|
+
}
|
|
151
|
+
const editorStopped = () => { const b = document.querySelector("#canvas-viewport > .me-open"); if (b) b.hidden = false; };
|
|
152
|
+
|
|
153
|
+
return { guess, sheetTools, selectRow, attach, editorStopped };
|
|
154
|
+
})();
|