sagadeck 1.8.0 → 1.10.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 +25 -8
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +482 -7
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +178 -111
- package/package.json +35 -4
- package/src/ai/commands.js +2 -2
- package/src/ai/context.js +7 -2
- package/src/ai/deck-ai.js +4 -2
- package/src/ai/ia-check.js +127 -0
- package/src/ai/ia-config.js +2 -2
- package/src/ai/llm.js +5 -1
- package/src/ai/map-analysis-run.js +1 -1
- package/src/ai/quality.js +1 -1
- package/src/ai/video-generation.js +70 -25
- package/src/api-client.js +42 -10
- package/src/build.js +46 -14
- package/src/code-highlight.js +19 -2
- package/src/code-language.js +13 -0
- package/src/elements.js +39 -1
- package/src/export/pptx.js +7 -2
- package/src/export/shots.js +7 -2
- package/src/fiscal/normalize.js +19 -0
- package/src/fs-retry.js +37 -2
- package/src/import/office.js +5 -4
- package/src/layouts.js +104 -99
- package/src/lessons.js +1 -1
- package/src/library.js +7 -7
- package/src/map-analysis.js +4 -1
- package/src/map-config.js +10 -4
- package/src/map.js +21 -8
- package/src/mcp/server.js +17 -0
- package/src/motion.js +20 -6
- package/src/purpose.js +1 -1
- package/src/runtime/api-core.js +122 -11
- package/src/runtime/api-ui.js +2 -1
- package/src/runtime/base.css +88 -332
- package/src/runtime/fit.js +3 -2
- package/src/runtime/fonts/amatic-sc.css +3 -0
- package/src/runtime/fonts/bebas-neue.css +2 -0
- package/src/runtime/fonts/cabin-sketch.css +2 -0
- package/src/runtime/fonts/fraunces.css +3 -0
- package/src/runtime/fonts/gelasio.css +3 -0
- package/src/runtime/fonts/ibm-plex-mono.css +4 -0
- package/src/runtime/fonts/index.json +10 -1
- package/src/runtime/fonts/itim.css +2 -0
- package/src/runtime/fonts/montserrat.css +2 -0
- package/src/runtime/fonts/space-grotesk.css +2 -0
- package/src/runtime/map-background.js +91 -0
- package/src/runtime/map.js +41 -9
- package/src/runtime/runtime.js +81 -100
- package/src/runtime/skins/aurora-dev.css +39 -0
- package/src/runtime/skins/caderno.css +33 -0
- package/src/runtime/skins/cartaz.css +27 -0
- package/src/runtime/skins/infografico.css +29 -0
- package/src/runtime/skins/lousa.css +22 -0
- package/src/runtime/skins/musgo.css +42 -0
- package/src/runtime/skins/terminal-pro.css +33 -0
- package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
- package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
- package/src/runtime/vendor/geo-contornos.json +1 -1
- package/src/runtime/vendor/maplibre-gl.css +1 -0
- package/src/runtime/vendor/maplibre-gl.js +825 -0
- package/src/runtime/vendor/maplibre-leaflet.js +234 -0
- package/src/runtime/vendor/maplibre-worker.js +10 -0
- package/src/studio/ai-chat-route.js +26 -7
- package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
- package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
- package/src/studio/command-approval.js +2 -3
- package/src/studio/demo-decks.js +4 -2
- package/src/studio/developer-starters.js +46 -0
- package/src/studio/export-jobs.js +86 -0
- package/src/studio/exporting.js +36 -18
- package/src/studio/feature-demos.js +144 -0
- package/src/studio/ia-routes.js +9 -0
- package/src/studio/layout-samples.js +9 -18
- package/src/studio/library-routes.js +7 -9
- package/src/studio/project.js +1 -0
- package/src/studio/public/about.js +68 -0
- package/src/studio/public/ai-settings.js +2 -2
- package/src/studio/public/app.js +43 -52
- package/src/studio/public/chat.js +5 -1
- package/src/studio/public/developer-authoring.css +12 -0
- package/src/studio/public/developer-authoring.js +306 -0
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/generation-guide.js +42 -0
- package/src/studio/public/index.html +31 -10
- package/src/studio/public/library.css +48 -3
- package/src/studio/public/library.html +5 -1
- package/src/studio/public/library.js +150 -41
- package/src/studio/public/map-data.js +7 -3
- package/src/studio/public/map-editor.js +625 -236
- package/src/studio/public/map-settings.js +3 -1
- package/src/studio/public/slide-form.js +27 -29
- package/src/studio/public/style.css +56 -9
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +29 -14
- package/src/studio/snapshot.js +8 -4
- package/src/studio/template-collections.js +1 -1
- package/src/studio/tour.js +89 -7
- package/src/themes.js +202 -0
- package/templates/cenario-e-ciencia.yaml +1 -21
- package/templates/ensaio-api.yaml +3 -3
- package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
- package/templates/cenario/imagens/cidade.svg +0 -30
- package/templates/cenario/imagens/pessoa.svg +0 -15
package/src/import/office.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { removeRetry } from "../fs-retry.js";
|
|
11
12
|
|
|
12
13
|
const isWin = process.platform === "win32";
|
|
13
14
|
const tmpDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "sagadeck-import-"));
|
|
@@ -20,7 +21,7 @@ function powershell(script, args, timeout = 600000) {
|
|
|
20
21
|
fs.writeFileSync(json, JSON.stringify(args), "utf8");
|
|
21
22
|
const r = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", ps1, json], { encoding: "utf8", timeout, windowsHide: true });
|
|
22
23
|
return { ok: r.status === 0, out: (r.stdout || "").trim(), err: (r.stderr || "").trim() };
|
|
23
|
-
} finally {
|
|
24
|
+
} finally { removeRetry(dir); }
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
let officeCache = null;
|
|
@@ -75,7 +76,7 @@ foreach ($p in $a.pairs) {
|
|
|
75
76
|
spawnSync(lo, ["--headless", "--convert-to", "png", "--outdir", out, p.src], { timeout: 120000 });
|
|
76
77
|
const f = fs.readdirSync(out).find((x) => x.endsWith(".png"));
|
|
77
78
|
if (f) fs.copyFileSync(path.join(out, f), p.dst);
|
|
78
|
-
|
|
79
|
+
removeRetry(out);
|
|
79
80
|
}
|
|
80
81
|
return pairs.filter((p) => fs.existsSync(p.dst)).map((p) => p.dst);
|
|
81
82
|
}
|
|
@@ -108,7 +109,7 @@ try {
|
|
|
108
109
|
try {
|
|
109
110
|
const r = powershell(script, { file: copy, out: outDir, width });
|
|
110
111
|
if (!r.ok) return [];
|
|
111
|
-
} finally {
|
|
112
|
+
} finally { removeRetry(work); }
|
|
112
113
|
return fs.readdirSync(outDir).filter((f) => /^slide-\d+\.png$/.test(f)).sort().map((f) => path.join(outDir, f));
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -119,5 +120,5 @@ export function libreOfficeToPdf(file) {
|
|
|
119
120
|
const out = tmpDir();
|
|
120
121
|
spawnSync(lo, ["--headless", "--convert-to", "pdf", "--outdir", out, file], { timeout: 300000 });
|
|
121
122
|
const f = fs.readdirSync(out).find((x) => x.endsWith(".pdf"));
|
|
122
|
-
return f ? { pdf: path.join(out, f), cleanup: () =>
|
|
123
|
+
return f ? { pdf: path.join(out, f), cleanup: () => removeRetry(out) } : null;
|
|
123
124
|
}
|
package/src/layouts.js
CHANGED
|
@@ -12,7 +12,7 @@ import { adaptiveHTML } from "./adaptive-layouts.js";
|
|
|
12
12
|
import { iconSVG, listIcons } from "./figures/icons.js";
|
|
13
13
|
let iconNames = null;
|
|
14
14
|
const diagramIconNames = () => (iconNames ||= new Set(listIcons()));
|
|
15
|
-
import { el, text, figureHTML, imageSize, attrs, SIZES, list, cards, stats, steps, poll, timer, counter, code } from "./elements.js";
|
|
15
|
+
import { el, text, figureHTML, imageSize, attrs, SIZES, list, cards, stats, steps, poll, timer, counter, code, popHTML } from "./elements.js";
|
|
16
16
|
import { displayCodeLanguage, resolveCodeLanguage } from "./code-language.js";
|
|
17
17
|
import { mathHTML, plotHTML } from "./science.js";
|
|
18
18
|
import { infographicHTML, itemColors } from "./infographic.js";
|
|
@@ -40,44 +40,11 @@ const codeEditor = (s, { source, highlight = s.highlight, footer = "CÓDIGO · L
|
|
|
40
40
|
};
|
|
41
41
|
// As cenas de aula deixam todo o conteúdo no HTML: funcionam offline e têm resumo estático.
|
|
42
42
|
const lessonSteps = (items, fallback) => Array.isArray(items) && items.length ? items.map((p) => typeof p === "string" ? { title: p } : (p || {})) : [fallback];
|
|
43
|
-
const kineticPhrases = (value) => {
|
|
44
|
-
const words = String(value || "").match(/==[^=]+==|\*\*.*?\*\*|\*[^*]+\*|~~.*?~~|`[^`]+`|\[[^\]]+\]\([^)]+\)|\S+/g) || [];
|
|
45
|
-
const connectors = new Set(["a", "as", "de", "do", "da", "dos", "das", "e", "em", "no", "na", "nos", "nas", "para", "por", "com", "o", "os"]);
|
|
46
|
-
const phrases = [];
|
|
47
|
-
while (words.length) {
|
|
48
|
-
let take = Math.ceil(words.length / Math.ceil(words.length / 4));
|
|
49
|
-
const last = words[take - 1]?.replace(/^[^A-Za-zÀ-ÿ]+|[^A-Za-zÀ-ÿ]+$/g, "").toLowerCase();
|
|
50
|
-
if (take > 1 && connectors.has(last)) take--;
|
|
51
|
-
phrases.push(words.splice(0, take).join(" "));
|
|
52
|
-
}
|
|
53
|
-
return phrases;
|
|
54
|
-
};
|
|
55
43
|
const lessonControls = (items) => `<nav class="lesson-controls" aria-label="Etapas da explicação"><button type="button" data-lesson-prev aria-label="Etapa anterior" disabled>${iconSVG("arrow-left", { size: 26, stroke: 2 })}</button><div class="lesson-dots">${items.map((p, i) => `<button type="button" data-lesson-go="${i}" aria-label="Etapa ${i + 1}: ${esc(p.title || "Explicação")}" aria-current="${i === 0 ? "step" : "false"}">${String(i + 1).padStart(2, "0")}</button>`).join("")}</div><button type="button" data-lesson-next aria-label="Próxima etapa" ${items.length === 1 ? "disabled" : ""}>${iconSVG("arrow-right", { size: 26, stroke: 2 })}</button></nav>`;
|
|
56
44
|
const lessonPanels = (items, output = false) => `<div class="lesson-panels" aria-live="polite" aria-atomic="true">${items.map((p, i) => `<article class="lesson-panel${i === 0 ? " active" : ""}" data-lesson-panel="${i}" data-highlight="${esc(JSON.stringify([].concat(p.highlight || []).filter(Number.isFinite)))}"><div class="lesson-counter f-label">${String(i + 1).padStart(2, "0")} / ${String(items.length).padStart(2, "0")}</div>${text(p.title || `Etapa ${i + 1}`, "h3", { class: "lesson-title", size: 48 })}${p.text ? text(p.text, "body", { class: "lesson-text", size: 32 }) : ""}${output && p.output != null ? `<div class="lesson-output"><div class="lesson-output-label f-label">Saída esperada · simulação</div><pre class="f-mono">${esc(p.output)}</pre></div>` : ""}</article>`).join("")}</div>`;
|
|
57
45
|
const lessonSummary = (items) => `<div class="lesson-summary">${items.map((p, i) => `<article><div class="f-label lesson-summary-number">${String(i + 1).padStart(2, "0")}</div><div><strong class="f-heading">${md(p.title || `Etapa ${i + 1}`)}</strong>${p.text ? `<p class="f-body">${md(p.text)}</p>` : ""}${p.output != null ? `<pre class="f-mono">${esc(p.output)}</pre>` : ""}</div></article>`).join("")}</div>`;
|
|
58
46
|
const percent = (value, fallback, min = 0, max = 100) => Number.isFinite(Number(value)) ? Math.max(min, Math.min(max, Number(value))) : fallback;
|
|
59
47
|
|
|
60
|
-
// Texto no cenário: as composições. label = nome no formulário; size = tamanho padrão do título (titleSize vence);
|
|
61
|
-
// props = cenografia extra (só texto e caixas; os desenhos são CSS em base.css, nada de símbolo unicode).
|
|
62
|
-
const candles = () => Array.from({ length: 30 }, (_, i) => {
|
|
63
|
-
const y = 120 + Math.round(Math.sin(i * 0.55) * 90 + i * 11), h = 40 + ((i * 37) % 90), up = (i * 7) % 3 !== 0;
|
|
64
|
-
return `<i class="candle ${up ? 'up' : 'down'}" style="--x:${i * 64}px;--y:${y}px;--h:${h}px"></i>`;
|
|
65
|
-
}).join('');
|
|
66
|
-
export const SCENES = {
|
|
67
|
-
stage: { label: 'Palco e profundidade', size: 230, tone: 'dark' },
|
|
68
|
-
floor: { label: 'Chão em perspectiva', size: 230 },
|
|
69
|
-
signs: { label: 'Placas na cidade', size: 230, tone: 'dark' },
|
|
70
|
-
terminal: { label: 'Terminal hacker', size: 150, tone: 'dark', props: () => `<pre class="scene-code">${Array.from({ length: 34 }, (_, i) => `${(0x7f3a00 + i * 16).toString(16)} ${Array.from({ length: 12 }, (_, j) => ((i * 31 + j * 17) % 256).toString(16).padStart(2, '0')).join(' ')} ${['ssh saga@deck','sudo apresentar --ao-vivo','git push origin ideia','cat segredo.txt | grep plano','nmap -sV palco.local','tail -f plateia.log'][i % 6]}`).join('\n')}</pre><div class="scene-scan"></div>` },
|
|
71
|
-
cafe: { label: 'Lousa de café', size: 170, tone: 'dark', props: () => `<div class="scene-neon">aberto</div><ul class="scene-menu"><li><span>espresso</span><span>7</span></li><li><span>coado do dia</span><span>9</span></li><li><span>pão de queijo</span><span>6</span></li><li><span>ideia nova</span><span>grátis</span></li></ul>` },
|
|
72
|
-
travel: { label: 'Cartão de embarque', size: 140, props: () => `<div class="scene-pass"><div class="scene-pass-top">CARTÃO DE EMBARQUE</div><dl><div><dt>DE</dt><dd>GRU</dd></div><div><dt>PARA</dt><dd>LIS</dd></div><div><dt>VOO</dt><dd>SG 2026</dd></div><div><dt>PORTÃO</dt><dd>12</dd></div><div><dt>ASSENTO</dt><dd>7A</dd></div><div><dt>EMBARQUE</dt><dd>09:40</dd></div></dl><div class="scene-barcode"></div></div>` },
|
|
73
|
-
ticker: { label: 'Pregão financeiro', size: 190, tone: 'dark', props: () => `<div class="scene-tape"><span>IBOV 128.430 <b class="up">+1,24%</b></span><span>USD/BRL 5,02 <b class="down">-0,31%</b></span><span>SAGA3 42,10 <b class="up">+8,90%</b></span><span>IDEIA11 99,00 <b class="up">+12,5%</b></span><span>BTC 312.004 <b class="down">-2,05%</b></span><span>SELIC 10,50%</span><span>IBOV 128.430 <b class="up">+1,24%</b></span></div><div class="scene-candles">${candles()}</div>` },
|
|
74
|
-
marquee: { label: 'Letreiro de cinema', size: 150, tone: 'dark', props: () => `<div class="scene-curtain left"></div><div class="scene-curtain right"></div><div class="scene-ticket">SESSÃO 20H</div>` },
|
|
75
|
-
blueprint: { label: 'Planta técnica', size: 170, tone: 'accent', props: () => `<div class="scene-dim h"><span>1920</span></div><div class="scene-dim v"><span>1080</span></div><table class="scene-stamp"><tr><td>PROJETO</td><td>SAGADECK</td></tr><tr><td>FOLHA</td><td>01 / 13</td></tr><tr><td>ESCALA</td><td>1:1</td></tr><tr><td>REVISÃO</td><td>A</td></tr></table>` },
|
|
76
|
-
magazine: { label: 'Capa de revista', size: 210, props: () => `<div class="scene-masthead">SAGA</div><div class="scene-issue">EDIÇÃO 13 <i></i> SETEMBRO 2026 <i></i> R$ 24,90</div><ul class="scene-lines"><li><b>+10</b> ideias que mudam o jogo</li><li><b>Exclusivo</b> os bastidores do palco</li><li><b>Guia</b> como contar uma história</li></ul><div class="scene-barcode"></div>` },
|
|
77
|
-
orbit: { label: 'Órbita', size: 170, tone: 'dark', props: () => `<div class="scene-stars"></div><div class="scene-stars far"></div><div class="scene-hud"><span>LAT -23.55</span><span>LON -46.63</span><span>ALT 408 KM</span><span>V 7,66 KM/S</span></div>` },
|
|
78
|
-
synthwave: { label: 'Neon anos 80', size: 190, tone: 'dark', props: () => `<div class="scene-sun"></div><div class="scene-grid"></div><div class="scene-mountains"></div>` },
|
|
79
|
-
gallery: { label: 'Parede de galeria', size: 170, props: () => `<div class="scene-spot"></div><div class="scene-plaque"><b>Sem título</b><span>2026. Texto sobre parede.</span><span>Coleção particular</span></div><div class="scene-baseboard"></div>` },
|
|
80
|
-
};
|
|
81
48
|
|
|
82
49
|
// No blocks, "rótulo e depois o gráfico" é o título do gráfico, e dois ou mais gráficos seguidos ficam lado a lado:
|
|
83
50
|
// empilhados, cada um ganhava 200 e poucos px de altura e, mantendo a proporção, saía estreito no meio do slide.
|
|
@@ -169,39 +136,119 @@ export const LAYOUTS = {
|
|
|
169
136
|
${s.text ? `<div class="df-rule e" style="--d:2;"></div>${text(s.text, "lead", { class: "df-text e", style: "--d:3;", step: s.build ? 2 : undefined })}` : ""}</div>${fig}</div>${src(s)}${add(s, ctx)}`;
|
|
170
137
|
},
|
|
171
138
|
|
|
172
|
-
// Calendário (pedido: "um infográfico em forma de calendário dos próximos filmes")
|
|
173
|
-
// o mini calendário
|
|
174
|
-
//
|
|
139
|
+
// Calendário (pedido: "um infográfico em forma de calendário dos próximos filmes"). Visões (`view`):
|
|
140
|
+
// - meses: um cartão por mês com o mini calendário (os dias marcados na cor do evento) e a lista embaixo;
|
|
141
|
+
// - mes: um mês grande, como calendário de parede, com os eventos escritos nos dias (muitos eventos num mês);
|
|
142
|
+
// - semana: uma coluna por dia, com o horário (programação de um evento, semana de lançamento);
|
|
143
|
+
// - lista: agenda compacta agrupada por mês, em colunas (muitos meses cheios).
|
|
144
|
+
// Sem `view`, escolhe sozinho: cabe nos cartões, meses; um mês só que não cabe, mes; vários meses cheios, lista
|
|
145
|
+
// (pedido de Narumi: muitos eventos no mesmo mês enchiam o cartão e passavam da altura do slide). `month` (2026-11)
|
|
146
|
+
// escolhe o mês da visão mes; `week` (uma data) a semana da visão semana. Datas: 2026-12-18, 18/12/2026, 2026-12
|
|
147
|
+
// (só o mês) ou nada; hora em `time` ("14:00") ou junto da data ("2026-12-18 14:00").
|
|
175
148
|
calendar(s, ctx) {
|
|
176
149
|
const MESES = ["jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"];
|
|
150
|
+
const MESES_LONGOS = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
|
|
151
|
+
const DIAS = ["dom", "seg", "ter", "qua", "qui", "sex", "sáb"];
|
|
152
|
+
const warn = (m) => ctx?.warnings?.push(`calendário: ${m}`);
|
|
177
153
|
const parse = (d) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
154
|
+
const raw = String(d ?? "").trim();
|
|
155
|
+
const hm = raw.match(/[ T](\d{1,2})[:h](\d{2})$/);
|
|
156
|
+
const t = hm ? raw.slice(0, hm.index).trim() : raw;
|
|
157
|
+
const time = hm ? `${hm[1].padStart(2, "0")}:${hm[2]}` : null;
|
|
158
|
+
let m = t.match(/^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?$/); if (m) return { y: +m[1], mo: +m[2] - 1, d: m[3] ? +m[3] : null, time };
|
|
159
|
+
m = t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (m) return { y: +m[3], mo: +m[2] - 1, d: +m[1], time };
|
|
160
|
+
m = t.match(/^(\d{1,2})\/(\d{4})$/); if (m) return { y: +m[2], mo: +m[1] - 1, d: null, time };
|
|
182
161
|
return null;
|
|
183
162
|
};
|
|
163
|
+
const timeOf = (e) => (e.time != null && String(e.time).trim() ? String(e.time).trim().replace("h", ":") : e.at?.time || "");
|
|
184
164
|
const events = (Array.isArray(s.events) ? s.events : []).filter((e) => e && typeof e === "object").map((e, i) => ({ ...e, i, at: parse(e.date) }));
|
|
185
165
|
const undated = [...events.filter((e) => !e.at || e.at.mo < 0 || e.at.mo > 11), ...(Array.isArray(s.undated) ? s.undated.map((e) => (typeof e === "string" ? { title: e } : e)) : [])];
|
|
166
|
+
const dated = events.filter((x) => x.at && x.at.mo >= 0 && x.at.mo <= 11).sort((a, b) => a.at.y - b.at.y || a.at.mo - b.at.mo || (a.at.d || 99) - (b.at.d || 99) || timeOf(a).localeCompare(timeOf(b)));
|
|
186
167
|
const months = new Map();
|
|
187
|
-
for (const e of
|
|
188
|
-
const key = `${e.at.y}-${e.at.mo}`; if (!months.has(key)) months.set(key, { y: e.at.y, mo: e.at.mo, list: [] }); months.get(key).list.push(e);
|
|
189
|
-
}
|
|
168
|
+
for (const e of dated) { const key = `${e.at.y}-${e.at.mo}`; if (!months.has(key)) months.set(key, { y: e.at.y, mo: e.at.mo, list: [] }); months.get(key).list.push(e); }
|
|
190
169
|
const colors = itemColors(ctx?.theme || { colors: {} }, 6).map((h) => `#${h}`); // as do infográfico: do tema, diferentes entre si e com contraste
|
|
191
170
|
const tagColor = new Map(); const colorOf = (e) => e.color ? (/^#?[0-9a-f]{6}$/i.test(e.color) ? `#${String(e.color).replace("#", "")}` : /^[a-z][\w-]*$/i.test(e.color) ? `var(--${e.color})` : "var(--hi)") : (e.tag ? (tagColor.has(e.tag) || tagColor.set(e.tag, colors[tagColor.size % colors.length]), tagColor.get(e.tag)) : colors[0]);
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
171
|
+
const step = (k) => (s.build ? ` data-step="${k + 1}"` : "");
|
|
172
|
+
let list = [...months.values()];
|
|
173
|
+
const und = (k) => undated.length ? `<section class="ca-month ca-undated e" style="--d:${2 + k};"><header class="ca-head"><span class="t f-display ca-mo">${md(String(s.undatedTitle || "sem data"))}</span></header><ul class="ca-list">${undated.map((e) => `<li class="ca-ev" style="--c:var(--muted)"><span class="t f-label ca-day">?</span><div class="ca-what">${text(e.title || "", "h3", { class: "ca-title", size: 24 })}${e.text ? text(e.text, "small", { class: "ca-text muted", size: 17 }) : ""}</div></li>`).join("")}</ul></section>` : "";
|
|
174
|
+
|
|
175
|
+
// cabe nos cartões? Medido no navegador: com título, a área dos cartões tem ~698 px (uma fileira); a lista começa
|
|
176
|
+
// ~245 px abaixo do topo do cartão num mês de 5 semanas (27 px a mais por semana); evento ocupa 27 px (54 com detalhe)
|
|
177
|
+
const nCards = list.length + (undated.length ? 1 : 0);
|
|
178
|
+
const cols = s.cols || Math.min(nCards, nCards <= 4 ? nCards : nCards <= 6 ? 3 : 4) || 1;
|
|
179
|
+
const rows = Math.ceil(nCards / cols) || 1;
|
|
180
|
+
const cardH = ((s.title || s.kicker ? 698 : 860) - (rows - 1) * 22) / rows;
|
|
181
|
+
const weeksOf = (m) => Math.ceil((new Date(Date.UTC(m.y, m.mo, 1)).getUTCDay() + new Date(Date.UTC(m.y, m.mo + 1, 0)).getUTCDate()) / 7);
|
|
182
|
+
const need = (m) => m.list.reduce((h, e) => h + (e.text ? 54 : 27), 0) + (m.list.length - 1) * 10;
|
|
183
|
+
const full = list.filter((m) => need(m) > cardH - 18 - 245 - (weeksOf(m) - 5) * 27 + 4);
|
|
184
|
+
let view = ["meses", "mes", "semana", "lista"].includes(s.view) ? s.view : null;
|
|
185
|
+
if (s.view && !view) warn(`visão "${s.view}" não existe (use meses, mes, semana ou lista)`);
|
|
186
|
+
if (!view) view = s.month ? "mes" : !full.length ? "meses" : list.length === 1 ? "mes" : "lista";
|
|
187
|
+
if (view === "meses" && full.length) warn(`${full.map((m) => `${MESES_LONGOS[m.mo]} tem ${m.list.length} eventos`).join(", ")} e não cabe no cartão; use view: mes (no Studio: Um slide por mês) ou view: lista`);
|
|
188
|
+
|
|
189
|
+
const chipOf = (e) => `<span class="cm-ev t f-body" style="--c:${colorOf(e)}">${timeOf(e) ? `<b>${esc(timeOf(e))}</b> ` : ""}${md(String(e.title || ""))}</span>`;
|
|
190
|
+
let body;
|
|
191
|
+
if (view === "mes") {
|
|
192
|
+
// o mês grande: `month` escolhe; senão os meses com evento (até dois lado a lado)
|
|
193
|
+
if (s.month) {
|
|
194
|
+
const want = parse(s.month);
|
|
195
|
+
if (!want) warn(`mês "${s.month}" não é uma data (use 2026-11)`);
|
|
196
|
+
else list = [months.get(`${want.y}-${want.mo}`) || { y: want.y, mo: want.mo, list: [] }];
|
|
197
|
+
}
|
|
198
|
+
if (list.length > 2) { warn(`${list.length} meses na visão de mês: mostrando os dois primeiros; use Um slide por mês (no Studio) ou view: lista`); list = list.slice(0, 2); }
|
|
199
|
+
const big = (m, k) => {
|
|
200
|
+
const first = new Date(Date.UTC(m.y, m.mo, 1)).getUTCDay(), days = new Date(Date.UTC(m.y, m.mo + 1, 0)).getUTCDate();
|
|
201
|
+
const weeks = Math.ceil((first + days) / 7), max = weeks > 5 ? 2 : 3;
|
|
202
|
+
const byDay = new Map(); for (const e of m.list.filter((x) => x.at.d)) (byDay.get(e.at.d) || byDay.set(e.at.d, []).get(e.at.d)).push(e);
|
|
203
|
+
const cells = Array.from({ length: weeks * 7 }, (_, j) => {
|
|
204
|
+
const d = j - first + 1;
|
|
205
|
+
if (d < 1 || d > days) return `<div class="cm-cell out"></div>`;
|
|
206
|
+
const evs = byDay.get(d) || [];
|
|
207
|
+
if (evs.length > max) warn(`dia ${d} de ${MESES_LONGOS[m.mo]} tem ${evs.length} eventos; ${evs.length - max} ficaram no "+ mais" (use view: semana para ver todos)`);
|
|
208
|
+
return `<div class="cm-cell${evs.length ? " has" : ""}"><span class="cm-d t f-label">${d}</span>${evs.slice(0, max).map(chipOf).join("")}${evs.length > max ? `<span class="cm-more t f-label">+ ${evs.length - max} mais</span>` : ""}</div>`;
|
|
209
|
+
}).join("");
|
|
210
|
+
const noDay = m.list.filter((x) => !x.at.d);
|
|
211
|
+
return `<section class="cm-month e" style="--d:${2 + k};--weeks:${weeks}"${step(k)}><header class="ca-head"><span class="t f-display ca-mo">${MESES_LONGOS[m.mo]}</span><span class="t f-label ca-y">${m.y}</span></header>
|
|
212
|
+
<div class="cm-grid">${DIAS.map((d) => `<u class="t f-label">${d}</u>`).join("")}${cells}</div>${noDay.length ? `<div class="cm-noday"><span class="t f-label">no mês</span>${noDay.map(chipOf).join("")}</div>` : ""}</section>`;
|
|
213
|
+
};
|
|
214
|
+
body = `<div class="cm-months" style="--cols:${list.length || 1}">${list.map(big).join("")}${undated.length ? `<div class="cm-side">${und(list.length)}</div>` : ""}</div>`;
|
|
215
|
+
} else if (view === "semana") {
|
|
216
|
+
// semanas (de domingo a sábado) com evento de dia marcado; `week` escolhe uma
|
|
217
|
+
const startOf = (y, mo, d) => { const t = Date.UTC(y, mo, d); return t - new Date(t).getUTCDay() * 864e5; };
|
|
218
|
+
const withDay = dated.filter((e) => e.at.d);
|
|
219
|
+
let starts = [...new Set(withDay.map((e) => startOf(e.at.y, e.at.mo, e.at.d)))].sort((a, b) => a - b);
|
|
220
|
+
if (s.week) { const w = parse(s.week); if (w?.d) starts = [startOf(w.y, w.mo, w.d)]; else warn(`semana "${s.week}" precisa de um dia (2026-11-12)`); }
|
|
221
|
+
if (starts.length > 2) { warn(`${starts.length} semanas com evento: mostrando as duas primeiras; use week: (uma data) num slide por semana, ou view: lista`); starts = starts.slice(0, 2); }
|
|
222
|
+
if (!starts.length) warn("nenhum evento com dia marcado para a visão de semana");
|
|
223
|
+
const busy = (t0, j) => { const day = new Date(t0 + j * 864e5); return withDay.some((e) => e.at.y === day.getUTCFullYear() && e.at.mo === day.getUTCMonth() && e.at.d === day.getUTCDate()); };
|
|
224
|
+
const days = starts.some((t0) => busy(t0, 0) || busy(t0, 6)) ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5]; // fim de semana vazio sai
|
|
225
|
+
const week = (t0, k) => `<section class="cw-week e" style="--d:${2 + k};--days:${days.length}"${step(k)}>${days.map((j) => {
|
|
226
|
+
const day = new Date(t0 + j * 864e5), y = day.getUTCFullYear(), mo = day.getUTCMonth(), d = day.getUTCDate();
|
|
227
|
+
const evs = withDay.filter((e) => e.at.y === y && e.at.mo === mo && e.at.d === d);
|
|
228
|
+
return `<div class="cw-day${evs.length ? " has" : ""}"><header class="cw-h"><span class="t f-label">${DIAS[j]}</span><b class="t f-display">${d}</b><span class="t f-label cw-mo">${MESES[mo]}</span></header>
|
|
229
|
+
${evs.map((e) => `<div class="cw-ev" style="--c:${colorOf(e)}">${timeOf(e) ? `<span class="t f-label cw-t">${esc(timeOf(e))}</span>` : ""}${text(e.title || "", "h3", { class: "cw-title", size: 22 })}${e.text ? text(e.text, "small", { class: "cw-text muted", size: 16 }) : ""}${e.tag ? `<span class="t f-label ca-tag">${md(String(e.tag))}</span>` : ""}</div>`).join("")}</div>`;
|
|
230
|
+
}).join("")}</section>`;
|
|
231
|
+
body = `<div class="cw-weeks" style="--rows:${starts.length || 1}">${starts.map(week).join("")}</div>${undated.length ? `<div class="cm-noday"><span class="t f-label">${md(String(s.undatedTitle || "sem data"))}</span>${undated.map((e) => chipOf({ ...e, color: "muted" })).join("")}</div>` : ""}`;
|
|
232
|
+
} else if (view === "lista") {
|
|
233
|
+
// agenda: os meses em sequência, em colunas; a letra diminui com a quantidade
|
|
234
|
+
const total = dated.length + undated.length;
|
|
235
|
+
const ncol = s.cols || (total > 24 ? 3 : total > 10 ? 2 : 1);
|
|
236
|
+
if (total > 48) warn(`${total} eventos numa lista só ficam pequenos demais; divida em dois slides (no Studio: Um slide por mês)`);
|
|
237
|
+
const fs = total > 36 ? 15 : total > 20 ? 17 : total > 10 ? 20 : 24;
|
|
238
|
+
const row = (e) => `<li class="cl-ev" style="--c:${colorOf(e)}"><span class="cl-d"><b class="t f-display">${e.at?.d ? String(e.at.d).padStart(2, "0") : "—"}</b>${e.at?.d ? `<small class="t f-label">${DIAS[new Date(Date.UTC(e.at.y, e.at.mo, e.at.d)).getUTCDay()]}</small>` : ""}</span><div class="cl-what"><span class="cl-title t f-body">${timeOf(e) ? `<b class="cl-t">${esc(timeOf(e))}</b> ` : ""}${md(String(e.title || ""))}</span>${e.text ? `<small class="cl-text t f-body muted">${md(String(e.text))}</small>` : ""}</div>${e.tag ? `<span class="t f-label ca-tag">${md(String(e.tag))}</span>` : ""}</li>`;
|
|
239
|
+
body = `<div class="cl-list" style="--ncol:${ncol};--cal-fs:${fs}px">${list.map((m, k) => `<section class="cl-month e" style="--d:${2 + k};"${step(k)}><header class="cl-h"><span class="t f-display">${MESES_LONGOS[m.mo]}</span><span class="t f-label ca-y">${m.y}</span></header><ul>${m.list.map(row).join("")}</ul></section>`).join("")}${undated.length ? `<section class="cl-month e" style="--d:${2 + list.length};"><header class="cl-h"><span class="t f-display">${md(String(s.undatedTitle || "sem data"))}</span></header><ul>${undated.map((e) => row({ ...e, color: "muted" })).join("")}</ul></section>` : ""}</div>`;
|
|
240
|
+
} else {
|
|
241
|
+
const card = (m, k) => {
|
|
242
|
+
const first = new Date(Date.UTC(m.y, m.mo, 1)).getUTCDay(), days = new Date(Date.UTC(m.y, m.mo + 1, 0)).getUTCDate();
|
|
243
|
+
const byDay = new Map(m.list.filter((e) => e.at.d).map((e) => [e.at.d, e]));
|
|
244
|
+
const cells = [...Array(first).fill(`<i></i>`), ...Array.from({ length: days }, (_, j) => { const e = byDay.get(j + 1); return e ? `<b style="--c:${colorOf(e)}">${j + 1}</b>` : `<i>${j + 1}</i>`; })].join("");
|
|
245
|
+
const items = m.list.map((e) => `<li class="ca-ev" style="--c:${colorOf(e)}"><span class="t f-label ca-day">${e.at.d ? String(e.at.d).padStart(2, "0") : "—"}</span><div class="ca-what">${text(e.title || "", "h3", { class: "ca-title", size: 26 })}${e.text ? text(e.text, "small", { class: "ca-text muted", size: 18 }) : ""}${e.tag ? `<span class="t f-label ca-tag">${md(String(e.tag))}</span>` : ""}</div></li>`).join("");
|
|
246
|
+
return `<section class="ca-month e" style="--d:${2 + k};"${step(k)}><header class="ca-head"><span class="t f-display ca-mo">${MESES[m.mo]}</span><span class="t f-label ca-y">${m.y}</span></header><div class="ca-grid" aria-hidden="true">${"DSTQQSS".split("").map((d) => `<u>${d}</u>`).join("")}${cells}</div><ul class="ca-list">${items}</ul></section>`;
|
|
247
|
+
};
|
|
248
|
+
body = `<div class="ca-months" style="--cols:${cols};">${list.map(card).join("")}${und(list.length)}</div>`;
|
|
249
|
+
}
|
|
203
250
|
const legend = tagColor.size > 1 ? `<div class="ca-legend">${[...tagColor].map(([t, c]) => `<span class="t f-label" style="--c:${c}"><i></i>${md(String(t))}</span>`).join("")}</div>` : "";
|
|
204
|
-
return `<div class="L-calendar">${head(s)}${legend}
|
|
251
|
+
return `<div class="L-calendar" data-view="${view}">${head(s)}${legend}${body}</div>${src(s)}${add(s, ctx)}`;
|
|
205
252
|
},
|
|
206
253
|
|
|
207
254
|
// Pôster (infográfico de uma página, como uma figura de revista científica): ilustração principal, painéis com letra
|
|
@@ -393,7 +440,7 @@ export const LAYOUTS = {
|
|
|
393
440
|
const body = ev.map((e, i) => `<div${attrs({ step: e.step ?? build(s, i) }, `tl-ev ${hl.has(i) ? "hl" : ""}`)}>
|
|
394
441
|
<div class="tl-dot"></div><div class="tl-when t f-display">${md(e.when)}</div>
|
|
395
442
|
<div class="tl-title t f-heading">${md(e.title || "")}</div>${e.text ? `<div class="tl-text t f-body">${md(e.text)}</div>` : ""}
|
|
396
|
-
${e.tag ? `<div class="tl-tag t f-label">${md(e.tag)}</div>` : ""}</div>`).join("");
|
|
443
|
+
${e.tag ? `<div class="tl-tag t f-label">${md(e.tag)}</div>` : ""}${e.more ? popHTML(e.more, "above") : ""}</div>`).join("");
|
|
397
444
|
return `<div class="L-timeline">${head(s)}<div class="tl" style="--n:${ev.length}"><div class="tl-line"></div>${body}</div>${s.after ? text(s.after, "h3", { class: "tl-after", step: s.afterStep ?? (s.build ? ev.length + 1 : 1), face: "quote", size: 48 }) : ""}</div>${src(s)}${add(s, ctx)}`;
|
|
398
445
|
},
|
|
399
446
|
|
|
@@ -489,42 +536,6 @@ export const LAYOUTS = {
|
|
|
489
536
|
return `<div class="L-spotlight" data-lesson="spotlight" data-lesson-count="${spots.length}">${head(s)}<div class="lesson-row"><div class="spotlight-visual"><div class="spotlight-canvas"><div class="spotlight-zoom"><div class="spotlight-image">${el(visual, ctx, 1100, 660)}</div><div class="spotlight-regions">${regions}</div></div></div>${s.caption ? text(s.caption, "small", { class: "spotlight-caption", size: 24 }) : ""}</div><aside class="lesson-aside">${lessonPanels(spots)}${lessonSummary(spots)}${lessonControls(spots)}</aside></div></div>${src(s)}${add(s, ctx)}`;
|
|
490
537
|
},
|
|
491
538
|
|
|
492
|
-
kinetic(s, ctx) {
|
|
493
|
-
const beats = Array.isArray(s.beats) && s.beats.length
|
|
494
|
-
? s.beats.map((beat) => typeof beat === "string" ? { text: beat } : beat || {})
|
|
495
|
-
: [
|
|
496
|
-
...kineticPhrases(s.title || s.text),
|
|
497
|
-
...kineticPhrases(s.subtitle),
|
|
498
|
-
].map((text, i) => ({
|
|
499
|
-
text, style: "clean", position: "left", size: "medium",
|
|
500
|
-
...(i === 0 && s.kicker ? { tag: s.kicker } : {}),
|
|
501
|
-
}));
|
|
502
|
-
if (!beats.length) beats.push({ text: "Uma ideia em movimento", style: "clean", position: "left", color: "white", size: "large" });
|
|
503
|
-
const frames = beats.map((beat, i) => {
|
|
504
|
-
const style = ["clean", "poster", "editorial", "outline", "marker"].includes(beat.style) ? beat.style : "clean";
|
|
505
|
-
const position = ["left", "center", "right", "top", "bottom"].includes(beat.position) ? beat.position : "left";
|
|
506
|
-
const color = ["white", "gold", "pink", "cyan"].includes(beat.color) ? beat.color : "white";
|
|
507
|
-
const size = ["small", "medium", "large"].includes(beat.size) ? beat.size : "medium";
|
|
508
|
-
return `<article class="kinetic-frame${i === 0 ? " active" : ""}" data-lesson-panel="${i}" data-position="${position}" data-style="${style}" data-color="${color}" data-size="${size}" aria-hidden="${i !== 0}">
|
|
509
|
-
<div class="kinetic-word" data-fit>${md(beat.text || "")}</div>
|
|
510
|
-
${beat.tag ? `<div class="kinetic-tag">${md(beat.tag)}</div>` : ""}
|
|
511
|
-
</article>`;
|
|
512
|
-
}).join("");
|
|
513
|
-
const scene = s.figure ? el(s.figure, ctx, 1920, slideSize(ctx.spec).h) : "";
|
|
514
|
-
const interval = Math.max(450, Math.min(5000, Number(s.interval) || 1000));
|
|
515
|
-
const autoplay = s.autoplay !== false && beats.length > 1;
|
|
516
|
-
const summary = `<div class="kinetic-summary" aria-label="Frases da sequência">${beats.map((beat) => `<span>${md(beat.text || "")}</span>`).join("")}</div>`;
|
|
517
|
-
return `<div class="L-kinetic" data-lesson="kinetic" data-lesson-count="${beats.length}" data-kinetic-interval="${interval}" data-kinetic-autoplay="${autoplay}">
|
|
518
|
-
<div class="kinetic-scene" aria-hidden="true">${scene}</div><div class="kinetic-shade" aria-hidden="true"></div>
|
|
519
|
-
<div class="kinetic-grain" aria-hidden="true"></div>
|
|
520
|
-
<div class="kinetic-sequence" aria-live="polite" aria-atomic="true">${frames}</div>
|
|
521
|
-
<div class="kinetic-controls"><button type="button" class="kinetic-toggle" data-kinetic-toggle aria-pressed="${autoplay}" aria-label="${autoplay ? "Pausar" : "Reproduzir"} sequência"${beats.length < 2 ? " disabled" : ""}>${autoplay ? "Pausar" : "Reproduzir"}</button>
|
|
522
|
-
<nav class="kinetic-dots" aria-label="Frases da sequência">${beats.map((beat, i) => `<button type="button" data-lesson-go="${i}" aria-label="Frase ${i + 1}: ${esc(beat.text || "")}" aria-current="${i === 0 ? "step" : "false"}">${String(i + 1).padStart(2, "0")}</button>`).join("")}</nav>
|
|
523
|
-
<button type="button" class="kinetic-next" data-lesson-next aria-label="Próxima frase">${iconSVG("arrow-right", { size: 26, stroke: 2 })}</button>
|
|
524
|
-
</div>${summary}
|
|
525
|
-
</div>${add(s, ctx)}`;
|
|
526
|
-
},
|
|
527
|
-
|
|
528
539
|
blocks(s, ctx) {
|
|
529
540
|
return `<div class="L-blocks">${head(s, s.titleAs || "h2")}<div class="bl-body">${el(chartsSideBySide(s.content || []), ctx)}</div>${s.caption ? text(s.caption, "small", { class: "bl-caption muted" }) : ""}</div>${src(s)}${add(s, ctx)}`;
|
|
530
541
|
},
|
|
@@ -746,10 +757,4 @@ export const LAYOUTS = {
|
|
|
746
757
|
science(s, ctx) {
|
|
747
758
|
return `<div class="L-science">${head(s)}<div class="science-body${s.plot === false ? ' equations-only' : !(s.equations || []).length ? ' plot-only' : ''}"><div class="science-equations">${mathHTML(s.equations || [])}</div>${s.plot === false ? '' : plotHTML(s.plot || {}, ctx)}</div>${s.caption ? text(s.caption,'small') : ''}</div>${add(s,ctx)}`;
|
|
748
759
|
},
|
|
749
|
-
scenography(s, ctx) {
|
|
750
|
-
const scene = Object.hasOwn(SCENES, s.scene) ? s.scene : 'stage';
|
|
751
|
-
const op = s.imageOpacity == null || s.imageOpacity === '' ? 0.85 : Math.max(0, Math.min(1, Number(s.imageOpacity) || 0));
|
|
752
|
-
const props = SCENES[scene].props?.() || '';
|
|
753
|
-
return `<div class="L-scenography raster scene-${scene}"><div class="scene-atmosphere"></div>${s.image ? `<div class="scene-image" style="opacity:${op}">${el({image:s.image},ctx,1920,slideSize(ctx.spec).h)}</div>` : ''}${props ? `<div class="scene-props" aria-hidden="true">${props}</div>` : ''}<div class="scene-architecture" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div><div class="scene-type"><div class="scene-eyebrow t f-label">${md(s.kicker || 'IDEIAS QUE OCUPAM ESPAÇO')}</div>${text(s.title || 'ALÉM DO\nÓBVIO','hero',{class:'scene-title',fit:true,size:s.titleSize || SCENES[scene].size})}${s.subtitle ? text(s.subtitle,'lead',{class:'scene-subtitle'}) : ''}</div>${s.foreground ? `<div class="scene-foreground">${el({image:s.foreground,fit:'contain'},ctx,1920,1080)}</div>` : '<div class="scene-sculpture" aria-hidden="true"><i></i><b></b></div>'}<div class="scene-caption f-label">${esc(s.caption || 'SAGA / NOVAS PERSPECTIVAS')}</div></div>${add(s,ctx)}`;
|
|
754
|
-
},
|
|
755
760
|
};
|
package/src/lessons.js
CHANGED
|
@@ -34,7 +34,7 @@ function givenTex(g) {
|
|
|
34
34
|
if (g.latex) return g.latex;
|
|
35
35
|
const value = typeof g.value === "string" && isProse(g.value) ? `\\text{${g.value.replace(/[{}\\$]/g, "")}}` : commaTex(g.value);
|
|
36
36
|
// % (e # & _ $) são comandos no LaTeX: "60 %" saía como erro vermelho na conta da criança
|
|
37
|
-
const unit = g.unit ? String(g.unit).replace(/[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,") : "";
|
|
37
|
+
const unit = g.unit ? String(g.unit).replace(/(?<!\\)[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,") : "";
|
|
38
38
|
return `${g.symbol ?? ""} = ${value}${unit ? `\\ \\mathrm{${unit}}` : ""}`;
|
|
39
39
|
}
|
|
40
40
|
export function solutionHTML(s, ctx, head) {
|
package/src/library.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// O id de uma apresentação é o caminho do .yaml dela, relativo à biblioteca, com "/" (ex.: "Palestras/X/X.yaml").
|
|
16
16
|
import fs from "node:fs";
|
|
17
|
-
import { renameRetry } from "./fs-retry.js";
|
|
17
|
+
import { renameRetry, copyRetry, removeRetry } from "./fs-retry.js";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import { execFileSync } from "node:child_process";
|
|
20
20
|
import path from "node:path";
|
|
@@ -228,7 +228,7 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
228
228
|
const dir = topicDir(id);
|
|
229
229
|
const left = fs.readdirSync(dir).filter((n) => !hidden(n));
|
|
230
230
|
if (left.length) throw new Error("o tópico não está vazio: mova ou exclua as apresentações antes");
|
|
231
|
-
|
|
231
|
+
removeRetry(dir);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
// ---- apresentações ----
|
|
@@ -291,7 +291,7 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
291
291
|
const title = `${info.title} (cópia)`;
|
|
292
292
|
const ext = unit === file ? path.extname(file) : "";
|
|
293
293
|
const to = uniquePath(path.dirname(unit), safeName(title), ext);
|
|
294
|
-
|
|
294
|
+
copyRetry(unit, to);
|
|
295
295
|
let yaml = unit === file ? to : path.join(to, path.basename(file));
|
|
296
296
|
if (unit !== file && path.basename(file).replace(/\.ya?ml$/i, "") === path.basename(unit)) {
|
|
297
297
|
const renamed = path.join(to, path.basename(to) + path.extname(file));
|
|
@@ -404,11 +404,11 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
404
404
|
const ext = isFile ? path.extname(o.unit) : "";
|
|
405
405
|
const to = uniquePath(dest, path.basename(o.unit, ext), ext);
|
|
406
406
|
renameRetry(path.join(slot, o.unit), to);
|
|
407
|
-
|
|
407
|
+
removeRetry(slot);
|
|
408
408
|
return idOf(isFile ? to : path.join(to, o.yaml));
|
|
409
409
|
}
|
|
410
410
|
function purgeDeck(slotId) {
|
|
411
|
-
|
|
411
|
+
removeRetry(path.join(root, TRASH, path.basename(slotId)));
|
|
412
412
|
}
|
|
413
413
|
// esvaziar a lixeira: tudo de uma vez (a pessoa confirma antes, no Studio)
|
|
414
414
|
function emptyTrash() {
|
|
@@ -484,7 +484,7 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
484
484
|
if (!src.startsWith(path.resolve(fromDir)) || !fs.existsSync(src)) continue;
|
|
485
485
|
const dst = path.join(dir, ...rel.split("/"));
|
|
486
486
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
487
|
-
|
|
487
|
+
copyRetry(src, dst);
|
|
488
488
|
}
|
|
489
489
|
fs.writeFileSync(path.join(dir, "estilo.yaml"), YAML.stringify(style, { indent: 2, lineWidth: 0 }));
|
|
490
490
|
return { id, name: style.name };
|
|
@@ -505,7 +505,7 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
505
505
|
const to = `imagens/estilo/${slug(id)}/${path.basename(rel)}`;
|
|
506
506
|
const dst = path.join(deckDir, ...to.split("/"));
|
|
507
507
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
508
|
-
|
|
508
|
+
copyRetry(src, dst);
|
|
509
509
|
map.set(rel, to);
|
|
510
510
|
}
|
|
511
511
|
const master = structuredClone(style.master);
|
package/src/map-analysis.js
CHANGED
|
@@ -58,6 +58,7 @@ export function circle([lat, lon], metros, n = 32) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// faixa de `metros` em volta de uma linha, para DESENHAR: retângulos dos trechos e círculos nos vértices (MultiPolygon).
|
|
61
|
+
// Os pedaços se sobrepõem: a feature leva `_faixa: true` e o mapa a desenha sem contorno (uma mancha só).
|
|
61
62
|
// Para CONTAR o que está dentro, use pointLine (exato).
|
|
62
63
|
export function lineBuffer(geometry, metros) {
|
|
63
64
|
const lines = geometry.type === "LineString" ? [geometry.coordinates] : geometry.type === "MultiLineString" ? geometry.coordinates : [];
|
|
@@ -73,7 +74,9 @@ export function lineBuffer(geometry, metros) {
|
|
|
73
74
|
polys.push([[[lon1 + ox, lat1 + oy], [lon2 + ox, lat2 + oy], [lon2 - ox, lat2 - oy], [lon1 - ox, lat1 - oy], [lon1 + ox, lat1 + oy]]]);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
// todos no mesmo sentido (anti-horário): onde os pedaços se sobrepõem, a área preenche uma vez só, sem buraco
|
|
78
|
+
const ccw = (ring) => { let a = 0; for (let i = 0; i < ring.length - 1; i++) a += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1]; return a >= 0 ? ring : [...ring].reverse(); };
|
|
79
|
+
return { type: "MultiPolygon", coordinates: polys.map(([ring]) => [ccw(ring)]) };
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// "onde instalar o próximo": os `quantos` lugares mais longe de todos os existentes (e uns dos outros), dentro da área
|
package/src/map-config.js
CHANGED
|
@@ -12,9 +12,12 @@ export function mapaFile(env = process.env, home = os.homedir()) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const OSM = "© colaboradores do OpenStreetMap";
|
|
15
|
+
const OFM = '<a href="https://openfreemap.org/" target="_blank" rel="noopener">OpenFreeMap</a> · © <a href="https://openmaptiles.org/" target="_blank" rel="noopener">OpenMapTiles</a> · © <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">colaboradores do OpenStreetMap</a>';
|
|
15
16
|
export const MAP_DEFAULTS = {
|
|
16
|
-
//
|
|
17
|
+
// ruas conserva o OpenStreetMap; fundos de dados são vetoriais, sem a fronteira marítima desenhada no mar
|
|
17
18
|
tiles: { url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: OSM, maxZoom: 19 },
|
|
19
|
+
claro: { url: "https://tiles.openfreemap.org/styles/positron", tipo: "vetorial", attribution: OFM, maxZoom: 20 },
|
|
20
|
+
escuro: { url: "https://tiles.openfreemap.org/styles/dark", tipo: "vetorial", attribution: OFM, maxZoom: 20 },
|
|
18
21
|
// satélite só com provedor configurado (costuma pedir chave); sem ele, o slide usa as ruas e avisa
|
|
19
22
|
satelite: null,
|
|
20
23
|
geocoder: { provedor: "nominatim", url: "https://nominatim.openstreetmap.org", porSegundo: 1 },
|
|
@@ -37,7 +40,10 @@ export function loadMapConfig(file = mapaFile(), env = process.env) {
|
|
|
37
40
|
let raw = {};
|
|
38
41
|
try { raw = JSON.parse(fs.readFileSync(file, "utf8")) || {}; } catch { raw = {}; }
|
|
39
42
|
return Object.fromEntries(Object.entries(MAP_DEFAULTS).map(([k, base]) => {
|
|
40
|
-
|
|
43
|
+
let s = service(raw[k], base);
|
|
44
|
+
// arquivo antigo com tiles próprios (ou desligados): respeita esse fundo nos três modos
|
|
45
|
+
if (["claro", "escuro"].includes(k) && !Object.hasOwn(raw, k) && Object.hasOwn(raw, "tiles") && (raw.tiles === null || raw.tiles?.url && raw.tiles.url !== MAP_DEFAULTS.tiles.url)) s = service(raw.tiles, MAP_DEFAULTS.tiles);
|
|
46
|
+
if (s && ["tiles", "claro", "escuro", "satelite"].includes(k)) s = { ...s, tipo: s.url.includes("{z}") ? "raster" : s.tipo || "raster" };
|
|
41
47
|
if (s && !s.key && s.keyEnv && env[s.keyEnv]) return [k, { ...s, key: env[s.keyEnv], keyFrom: "variável" }];
|
|
42
48
|
return [k, s];
|
|
43
49
|
}));
|
|
@@ -78,6 +84,6 @@ export function maskedMapConfig(cfg) {
|
|
|
78
84
|
// o que pode ir para dentro do HTML da apresentação: só o fundo (o navegador busca os tiles). Chave de rotas e de
|
|
79
85
|
// geocodificação nunca vai para o deck.
|
|
80
86
|
export function tilesForDeck(cfg = loadMapConfig()) {
|
|
81
|
-
const pick = (t) => (t ? { url: t.url, attribution: t.attribution || OSM, maxZoom: t.maxZoom || 19 } : null);
|
|
82
|
-
return { tiles: pick(cfg.tiles), satelite: pick(cfg.satelite) };
|
|
87
|
+
const pick = (t) => (t ? { url: t.url, tipo: t.tipo || "raster", attribution: t.attribution || OSM, maxZoom: t.maxZoom || 19 } : null);
|
|
88
|
+
return { tiles: pick(cfg.tiles), claro: pick(cfg.claro === undefined ? cfg.tiles : cfg.claro), escuro: pick(cfg.escuro === undefined ? cfg.tiles : cfg.escuro), satelite: pick(cfg.satelite) };
|
|
83
89
|
}
|
package/src/map.js
CHANGED
|
@@ -110,9 +110,10 @@ export function fromKML(xml) {
|
|
|
110
110
|
|
|
111
111
|
function fromGeoJSON(obj) {
|
|
112
112
|
if (!obj || typeof obj !== "object") return [];
|
|
113
|
-
|
|
114
|
-
if (obj.type === "
|
|
115
|
-
if (obj.type
|
|
113
|
+
// _i: a posição da forma no arquivo (o editor do Studio acha a forma clicada por ela)
|
|
114
|
+
if (obj.type === "FeatureCollection") return (obj.features || []).map((f, i) => (f && f.geometry ? { ...f, properties: { ...f.properties, _i: i } } : null)).filter(Boolean);
|
|
115
|
+
if (obj.type === "Feature") return obj.geometry ? [{ ...obj, properties: { ...obj.properties, _i: 0 } }] : [];
|
|
116
|
+
if (obj.type && obj.coordinates) return [{ type: "Feature", properties: { _i: 0 }, geometry: obj }];
|
|
116
117
|
return [];
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -125,7 +126,7 @@ function fromAreas(values, warn) {
|
|
|
125
126
|
const uf = g.uf[code], pais = g.paises[code];
|
|
126
127
|
if (!uf && !pais) { warn(`mapa: "${k}" não é uma UF (SP, RJ…) nem um país (BR, PT…)`); continue; }
|
|
127
128
|
const poly = uf || pais.poly;
|
|
128
|
-
features.push({ type: "Feature", properties: { nome: uf ? code : pais.nome, valor: v }, geometry: { type: "MultiPolygon", coordinates: poly } });
|
|
129
|
+
features.push({ type: "Feature", properties: { nome: uf ? code : pais.nome, valor: v, _cod: String(k) }, geometry: { type: "MultiPolygon", coordinates: poly } });
|
|
129
130
|
}
|
|
130
131
|
return features;
|
|
131
132
|
}
|
|
@@ -204,6 +205,10 @@ export function mapModel(s, ctx) {
|
|
|
204
205
|
const head = [...new Set(rows.flatMap((p) => Object.keys(p)))];
|
|
205
206
|
features = pointsFromRows(head, rows.map((p) => head.map((h) => p[h] ?? "")), layer, warn).features;
|
|
206
207
|
}
|
|
208
|
+
} else if (layer.geojson && typeof layer.geojson === "object") {
|
|
209
|
+
// escrito no próprio slide (desenho pequeno, demonstração); dados de verdade ficam num arquivo do projeto
|
|
210
|
+
features = fromGeoJSON(layer.geojson);
|
|
211
|
+
if (!features.length) warn(`mapa: o GeoJSON da camada "${layer.name || i + 1}" não tem formas`);
|
|
207
212
|
} else if (layer.geojson) {
|
|
208
213
|
const text = readText(layer.geojson, ctx, "GeoJSON");
|
|
209
214
|
try { features = text == null ? [] : fromGeoJSON(JSON.parse(text)); } catch { warn(`mapa: "${layer.geojson}" não é um GeoJSON válido`); }
|
|
@@ -217,14 +222,22 @@ export function mapModel(s, ctx) {
|
|
|
217
222
|
if (!features.length && layer.areas == null) warn(`mapa: a camada "${layer.name || i + 1}" ficou vazia`);
|
|
218
223
|
const step = layer.step ?? (s.build ? i + 1 : undefined);
|
|
219
224
|
layers.push({
|
|
220
|
-
name: layer.name || layer.title || `Camada ${i + 1}`, kind,
|
|
221
|
-
source: typeof layer.points === "string" ? layer.points.trim() : layer.geojson || layer.gpx || layer.kml || null, step: Number.isFinite(Number(step)) ? Number(step) : undefined,
|
|
225
|
+
name: layer.name || layer.title || `Camada ${i + 1}`, kind, index: i, // index: a camada no slide (o editor do Studio)
|
|
226
|
+
source: typeof layer.points === "string" ? layer.points.trim() : (typeof layer.geojson === "string" ? layer.geojson : null) || layer.gpx || layer.kml || null, step: Number.isFinite(Number(step)) ? Number(step) : undefined,
|
|
222
227
|
view: viewOf(layer.view), legend: layer.legend || null, prefix: layer.prefix || "", suffix: layer.suffix || "",
|
|
223
228
|
style: styleOf(features, layer, warn), data: { type: "FeatureCollection", features },
|
|
224
229
|
});
|
|
225
230
|
});
|
|
226
231
|
const basemap = BASEMAPS.includes(s.basemap) ? s.basemap : "ruas";
|
|
227
232
|
if (s.basemap && !BASEMAPS.includes(s.basemap)) warn(`mapa: fundo "${s.basemap}" não existe (use ${BASEMAPS.join(", ")})`);
|
|
233
|
+
// camadas da mesma medida (a mesma coluna ou a mesma unidade na legenda) dividem uma escala: cada uma com a sua,
|
|
234
|
+
// a Bahia (2.900) saía tão forte quanto Minas (5.100) só por ser a maior da camada dela
|
|
235
|
+
const groups = new Map();
|
|
236
|
+
for (const l of layers) if (l.style.scale) { const k = `${l.style.colorBy}|${l.legend || ""}`; (groups.get(k) || groups.set(k, []).get(k)).push(l); }
|
|
237
|
+
for (const g of groups.values()) if (g.length > 1) {
|
|
238
|
+
const scale = { min: Math.min(...g.map((l) => l.style.scale.min)), max: Math.max(...g.map((l) => l.style.scale.max)) };
|
|
239
|
+
for (const l of g) l.style.scale = scale;
|
|
240
|
+
}
|
|
228
241
|
return { basemap, view: viewOf(s.view), bounds: bboxOf(layers), layers, tiles: ctx?.mapTiles || null };
|
|
229
242
|
}
|
|
230
243
|
|
|
@@ -249,11 +262,11 @@ export function mapPreviewSVG(model, w = 1600, h = 760) {
|
|
|
249
262
|
for (const layer of model.layers) for (const f of layer.data.features) {
|
|
250
263
|
const g = f.geometry, col = color(layer, f);
|
|
251
264
|
const lines = (cs) => cs.map((l) => `M${l.map(P).join("L")}`).join("");
|
|
252
|
-
if (g.type === "Point") { const [x, y] = P(g.coordinates).split(","); out.push(`<circle cx="${x}" cy="${y}" r="
|
|
265
|
+
if (g.type === "Point") { const [x, y] = P(g.coordinates).split(","); out.push(`<circle cx="${x}" cy="${y}" r="8" fill="${col}" stroke="#fff" stroke-width="3"/>`); }
|
|
253
266
|
else if (g.type === "LineString") out.push(`<path d="${lines([g.coordinates])}" fill="none" stroke="${col}" stroke-width="5"/>`);
|
|
254
267
|
else if (g.type === "MultiLineString") out.push(`<path d="${lines(g.coordinates)}" fill="none" stroke="${col}" stroke-width="5"/>`);
|
|
255
268
|
else if (g.type === "Polygon") out.push(`<path d="${lines(g.coordinates)}Z" fill="${col}" fill-opacity=".35" stroke="${col}" stroke-width="2"/>`);
|
|
256
|
-
else if (g.type === "MultiPolygon") out.push(`<path d="${g.coordinates.map((p) => lines(p) + "Z").join("")}" fill="${col}" fill-opacity=".35" stroke="${col}" stroke-width="2"/>`);
|
|
269
|
+
else if (g.type === "MultiPolygon") out.push(`<path d="${g.coordinates.map((p) => lines(p) + "Z").join("")}" fill="${col}" fill-opacity=".35" stroke="${col}" stroke-width="${f.properties?._faixa ? 0 : 2}"/>`);
|
|
257
270
|
}
|
|
258
271
|
return `<svg class="map-preview" viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet" aria-hidden="true">${out.join("")}</svg>`;
|
|
259
272
|
}
|
package/src/mcp/server.js
CHANGED
|
@@ -129,6 +129,18 @@ const TOOLS = [
|
|
|
129
129
|
required: ["query"],
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
name: "sagadeck_ia_check",
|
|
134
|
+
description: "Testa a IA configurada nesta máquina, sem tela: texto, visão (o modelo precisa dizer as cores de uma imagem), geração de imagem, pesquisa na web e vídeo, cada um com uma chamada de verdade, em paralelo. Rode antes de usar recursos de IA. Status por recurso: ok, falhou, nao_configurado ou sem_prova (vídeo sem gerar). Vídeo só é gerado de verdade com video_real: true (é cobrado).",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
so: { type: "array", items: { type: "string", enum: ["texto", "visao", "imagem", "pesquisa", "video"] }, description: "Só estes recursos (padrão: todos)" },
|
|
139
|
+
rapido: { type: "boolean", description: "Pula geração de imagem e vídeo (os lentos e cobrados)" },
|
|
140
|
+
video_real: { type: "boolean", description: "Gera um clipe curto de verdade e confere o MP4 (cobrado; leva minutos)" },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
132
144
|
{
|
|
133
145
|
name: "sagadeck_launch_studio",
|
|
134
146
|
description: "Inicia o SagaDeck Studio (página web estilo PowerPoint com canvas visual, edição WYSIWYG e chat lateral com IA).",
|
|
@@ -221,6 +233,11 @@ export function runMCPServer() {
|
|
|
221
233
|
|
|
222
234
|
async function handleToolCall(name, args) {
|
|
223
235
|
switch (name) {
|
|
236
|
+
case "sagadeck_ia_check": {
|
|
237
|
+
const { iaCheck } = await import("../ai/ia-check.js");
|
|
238
|
+
return iaCheck({ only: args.so, rapido: !!args.rapido, videoReal: !!args.video_real });
|
|
239
|
+
}
|
|
240
|
+
|
|
224
241
|
case "sagadeck_read_deck": {
|
|
225
242
|
const spec = loadSpec(path.resolve(args.path));
|
|
226
243
|
return {
|