sagadeck 1.2.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/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- package/templates/exemplo.yaml +228 -0
package/src/elements.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import {motionHTML} from './motion.js';
|
|
2
|
+
import { videoPlayer } from './video-player.js';
|
|
3
|
+
// Elementos: os "tijolos" que os layouts (e o layout livre `canvas`) usam.
|
|
4
|
+
// Todo elemento aceita: step (clique em que aparece), exit (clique em que some),
|
|
5
|
+
// anim (up|fade|pop|left|right|zoom|none), w, h, flex, align, class, style, card.
|
|
6
|
+
import { tableHTML } from "./table.js";
|
|
7
|
+
import { qrSVG } from "./figures/qr.js";
|
|
8
|
+
import katex from "katex";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { md, esc } from "./markup.js";
|
|
12
|
+
import { iconSVG } from "./figures/icons.js";
|
|
13
|
+
import { picto } from "./figures/pictos.js";
|
|
14
|
+
import { diagram } from "./figures/diagrams.js";
|
|
15
|
+
import { chart } from "./figures/charts.js";
|
|
16
|
+
import { parseTable, toNum } from "./science.js";
|
|
17
|
+
import { ufmap } from "./figures/ufmap.js";
|
|
18
|
+
import { pointsSVG } from "./figures/points.js";
|
|
19
|
+
import { highlightCode } from "./code-highlight.js";
|
|
20
|
+
import { resolveCodeLanguage } from "./code-language.js";
|
|
21
|
+
|
|
22
|
+
export const SIZES = { hero: 210, number: 250, title: 128, h2: 92, h3: 56, quote: 72, lead: 46, body: 36, small: 29, label: 24, mono: 30, tiny: 22 };
|
|
23
|
+
const FACE = { hero: "display", number: "display", title: "display", h2: "display", h3: "heading", quote: "quote", lead: "body", body: "body", small: "body", label: "label", mono: "mono", tiny: "label" };
|
|
24
|
+
|
|
25
|
+
const px = (v) => (v == null ? null : typeof v === "number" ? `${v}px` : String(v));
|
|
26
|
+
const colorVal = (c) => (!c ? null : /^#?[0-9a-f]{6}$/i.test(c) ? `#${String(c).replace("#", "")}` : `var(--${c})`);
|
|
27
|
+
// cor de texto: "hi" (cor de destaque) vira a versão legível sobre o fundo (--hi-ink, src/themes.js)
|
|
28
|
+
const textColorVal = (c) => (c === "hi" ? "var(--hi-ink,var(--hi))" : colorVal(c));
|
|
29
|
+
|
|
30
|
+
export function attrs(el = {}, extraCls = "", extraStyle = "") {
|
|
31
|
+
const cls = [extraCls, el.class, el.card ? "card" : "", el.card === "hi" ? "card-hi" : "", el.goto != null && el.goto !== "" ? "goto" : ""].filter(Boolean).join(" ");
|
|
32
|
+
let st = extraStyle;
|
|
33
|
+
if (el.w != null) st += `width:${px(el.w)};`;
|
|
34
|
+
if (el.h != null) st += `height:${px(el.h)};flex-shrink:0;`;
|
|
35
|
+
if (el.w != null && el.h != null) st += `flex:none;`;
|
|
36
|
+
if (el.flex != null) st += `flex:${el.flex};min-width:0;min-height:0;`;
|
|
37
|
+
if (el.color) st += `color:${textColorVal(el.color)};`;
|
|
38
|
+
if (el.bg) st += `background:${colorVal(el.bg)};`;
|
|
39
|
+
if (el.align) st += `text-align:${el.align};`;
|
|
40
|
+
if (el.pad != null) st += `padding:${px(el.pad)};`;
|
|
41
|
+
if (el.x != null) st += `position:absolute;left:${px(el.x)};top:${px(el.y ?? 0)};`;
|
|
42
|
+
if (el.opacity != null) st += `opacity:${el.opacity};`;
|
|
43
|
+
if (el.rotate) st += `transform:rotate(${el.rotate}deg);`;
|
|
44
|
+
if (el.style) st += el.style;
|
|
45
|
+
let a = "";
|
|
46
|
+
if (cls) a += ` class="${cls}"`;
|
|
47
|
+
if (st) a += ` style="${esc(st)}"`;
|
|
48
|
+
if (el.step != null && el.step > 0) a += ` data-step="${el.step}"`;
|
|
49
|
+
if (el.exit != null) a += ` data-exit="${el.exit}"`;
|
|
50
|
+
if (el.anim) a += ` data-anim="${el.anim}"`;
|
|
51
|
+
if (el.id) a += ` id="${esc(el.id)}"`;
|
|
52
|
+
if (el.continuity) a += ` data-continuity="${esc(el.continuity)}"`;
|
|
53
|
+
if (el.goto != null && el.goto !== "") a += ` data-goto="${esc(el.goto)}"`;
|
|
54
|
+
if (el.deco) a += ` data-locked="deco"`; // moldura/logos do original: no Studio o clique atravessa (destrava na lista de objetos)
|
|
55
|
+
return a;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Texto com papel tipográfico. `as` define tamanho+face; `size` sobrescreve px.
|
|
59
|
+
export function text(t, as = "body", el = {}) {
|
|
60
|
+
const role = el.as || as;
|
|
61
|
+
const face = el.face || FACE[role] || "body";
|
|
62
|
+
const size = el.size || SIZES[role] || 36;
|
|
63
|
+
const fit = el.fit ? " data-fit" : "";
|
|
64
|
+
const style = `font-size:${size}px;` + (el.weight ? `font-weight:${el.weight};` : "") + (el.upper ? "text-transform:uppercase;" : "") + (el.lh ? `line-height:${el.lh};` : "");
|
|
65
|
+
return `<div${attrs(el, `t f-${face} r-${role}`, style)}${fit}>${md(t)}</div>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// gráfico que lê um CSV ao lado do deck (csv: dados/vendas.csv): atualizou o arquivo, o slide atualiza.
|
|
69
|
+
// Primeira coluna = rótulo; uma coluna de números = data; várias = labels + series (linhas, colunas agrupadas).
|
|
70
|
+
function csvChart(el, ctx) {
|
|
71
|
+
if (typeof el.csv !== "string" || !el.csv.trim()) return el;
|
|
72
|
+
const file = path.resolve(ctx?.baseDir || process.cwd(), el.csv.trim());
|
|
73
|
+
let text;
|
|
74
|
+
try { text = fs.readFileSync(file, "utf8"); }
|
|
75
|
+
catch { ctx?.warnings?.push(`gráfico: "${el.csv}" não encontrado ao lado do deck — ficaram os dados do slide`); return el; }
|
|
76
|
+
const rows = parseTable(text);
|
|
77
|
+
const head = rows[0] && rows[0].slice(1).some((c) => toNum(c) == null) ? rows.shift() : null;
|
|
78
|
+
if (!rows.length) return el;
|
|
79
|
+
const cols = Math.max(...rows.map((r) => r.length)) - 1;
|
|
80
|
+
const names = Array.from({ length: cols }, (_, j) => head?.[j + 1] || `Série ${j + 1}`);
|
|
81
|
+
const { data, labels, series, csv, ...rest } = el;
|
|
82
|
+
if (cols === 1 && el.chart !== "line") return { ...rest, data: rows.map((r) => ({ label: r[0], value: toNum(r[1]) ?? 0 })) };
|
|
83
|
+
return { ...rest, labels: rows.map((r) => r[0]), series: names.map((name, j) => ({ name, values: rows.map((r) => toNum(r[j + 1])) })) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function figureHTML(el, ctx, w, h) {
|
|
87
|
+
if (el.points) {
|
|
88
|
+
try { return `<div${attrs(el, "fig")}>${pointsSVG(el.points)}</div>`; }
|
|
89
|
+
catch (e) { ctx.warnings?.push(String(e.message)); return `<div${attrs(el, "fig fig-pending fig-missing")} role="img" aria-label="${esc(String(e.message))}"><div class="fp-in"><span class="fp-tag f-label">figura de pontos indisponível</span><span class="fp-text f-body">${esc(String(e.message))}</span></div></div>`; }
|
|
90
|
+
}
|
|
91
|
+
if (el.qr) { const size = el.size || 360;
|
|
92
|
+
const svg = qrSVG(el.qr, { ec: el.ec, ink: el.ink || "#111", paper: el.paper || "#fff" });
|
|
93
|
+
return `<div${attrs(el, "fig fig-qr")}><div class="qr-box" style="width:${size}px;">${svg}</div>${el.label ? `<div class="qr-label f-label">${md(el.label)}</div>` : ""}</div>`;
|
|
94
|
+
}
|
|
95
|
+
if (el.icon) return `<div${attrs(el, "fig fig-icon", `color:${textColorVal(el.color) || "var(--fg)"};`)}>${iconSVG(el.icon, { size: el.size || 160, stroke: el.stroke || 1.6 })}</div>`;
|
|
96
|
+
if (el.picto) return `<div${attrs(el, "fig")}>${picto(el)}</div>`;
|
|
97
|
+
if (el.diagram) return `<div${attrs(el, "fig")}>${diagram(el)}</div>`;
|
|
98
|
+
// (com title: o nome do gráfico em cima, como a IA escreve quando põe dois lado a lado)
|
|
99
|
+
if (el.chart) return `<div${attrs(el, `fig fig-chart${el.title ? " has-title" : ""}`)}>${el.title ? `<div class="t f-label fc-title">${md(String(el.title))}</div>` : ""}${chart(csvChart(el, ctx), el.cw || w || 1200, el.ch || h || 620)}</div>`;
|
|
100
|
+
if (el.ufmap) return `<div${attrs(el, "fig fig-ufmap")}>${ufmap({ cw: w, ...el }, ctx?.warnings)}</div>`;
|
|
101
|
+
if (el.svg) return `<div${attrs(el, "fig")}>${el.svg}</div>`;
|
|
102
|
+
if (el.image) {
|
|
103
|
+
const src = imageSrc(el.image, ctx);
|
|
104
|
+
if (!src) return `<div${attrs(el, "fig fig-pending fig-missing")} role="img" aria-label="Imagem não encontrada: ${esc(el.image)}"><div class="fp-in"><span class="fp-tag f-label">imagem não encontrada</span><span class="fp-text f-body">${esc(el.image)}</span></div></div>`;
|
|
105
|
+
const flip = el.flipH || el.flipV ? `transform:scale(${el.flipH ? -1 : 1},${el.flipV ? -1 : 1});` : "";
|
|
106
|
+
// recorte (crop: l/t/r/b em fração do lado, como o srcRect do PowerPoint): a imagem cresce e a caixa corta
|
|
107
|
+
if (el.crop) {
|
|
108
|
+
const c = { l: +el.crop.l || 0, t: +el.crop.t || 0, r: +el.crop.r || 0, b: +el.crop.b || 0 };
|
|
109
|
+
const fw = Math.max(0.01, 1 - c.l - c.r), fh = Math.max(0.01, 1 - c.t - c.b);
|
|
110
|
+
return `<div${attrs(el, "fig fig-img fig-crop")}><img src="${src}" alt="${esc(el.alt || "")}" style="left:${(-c.l / fw) * 100}%;top:${(-c.t / fh) * 100}%;width:${100 / fw}%;height:${100 / fh}%;${flip}"></div>`;
|
|
111
|
+
}
|
|
112
|
+
// sem fit escolhido: preenche (cover), mas quando a proporção da imagem é muito diferente da caixa (fórmula, esquema
|
|
113
|
+
// largo numa coluna alta; gráfico com eixo e legenda na borda) cortar perderia informação: passou de 1,25× (corte de
|
|
114
|
+
// mais de 20%), cabe inteira (contain). Sem o tamanho da caixa aqui (figura do split, do layout), a mesma conta é feita
|
|
115
|
+
// na apresentação, com a caixa medida (data-autofit; src/runtime/fit.js: fitImages)
|
|
116
|
+
let fit = el.fit;
|
|
117
|
+
if (!fit) {
|
|
118
|
+
const sz = w && h ? imageSize(el.image, ctx) : null;
|
|
119
|
+
const box = w && h ? w / h : 0, img = sz && sz.h ? sz.w / sz.h : 0;
|
|
120
|
+
fit = box && img && Math.max(box / img, img / box) > 1.25 ? "contain" : "cover";
|
|
121
|
+
}
|
|
122
|
+
// imagem solta no fluxo, sem altura (num add, numa row; largura nenhuma ou em %): ganha a proporção do arquivo, cabe
|
|
123
|
+
// inteira e não passa de 420 px de altura (senão aparecia no tamanho natural do arquivo, enorme, e espremia o
|
|
124
|
+
// layout do slide até sumir)
|
|
125
|
+
let flow = "", box = el;
|
|
126
|
+
const pct = typeof el.w === "string" && /^\s*\d+(\.\d+)?%\s*$/.test(el.w);
|
|
127
|
+
if (!w && !h && (el.w == null || pct) && el.h == null && el.x == null) {
|
|
128
|
+
const sz = imageSize(el.image, ctx);
|
|
129
|
+
if (sz && sz.w && sz.h) {
|
|
130
|
+
flow = `aspect-ratio:${sz.w}/${sz.h};width:min(${pct ? el.w.trim() : "100%"}, ${Math.round((420 * sz.w) / sz.h)}px);`;
|
|
131
|
+
if (!el.fit) fit = "contain";
|
|
132
|
+
if (pct) { box = { ...el }; delete box.w; }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return `<div${attrs(box, "fig fig-img", flow)}><img src="${src}" alt="${esc(el.alt || "")}"${el.fit ? "" : " data-autofit"} style="object-fit:${fit};${el.radius ? `border-radius:${el.radius}px;` : ""}${flip}"></div>`;
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// largura × altura do arquivo de imagem (cabeçalho de PNG, JPEG, GIF, WebP) ou null
|
|
141
|
+
export function imageSize(p, ctx) {
|
|
142
|
+
try {
|
|
143
|
+
if (/^(https?:|data:)/.test(p)) return null;
|
|
144
|
+
const b = fs.readFileSync(path.resolve(ctx.baseDir, p));
|
|
145
|
+
if (b.readUInt32BE(0) === 0x89504e47) return { w: b.readUInt32BE(16), h: b.readUInt32BE(20) };
|
|
146
|
+
if (b.toString("ascii", 0, 3) === "GIF") return { w: b.readUInt16LE(6), h: b.readUInt16LE(8) };
|
|
147
|
+
if (b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP") {
|
|
148
|
+
const kind = b.toString("ascii", 12, 16);
|
|
149
|
+
if (kind === "VP8X") return { w: 1 + b.readUIntLE(24, 3), h: 1 + b.readUIntLE(27, 3) };
|
|
150
|
+
if (kind === "VP8 ") return { w: b.readUInt16LE(26) & 0x3fff, h: b.readUInt16LE(28) & 0x3fff };
|
|
151
|
+
if (kind === "VP8L") { const v = b.readUInt32LE(21); return { w: (v & 0x3fff) + 1, h: ((v >> 14) & 0x3fff) + 1 }; }
|
|
152
|
+
}
|
|
153
|
+
if (b[0] === 0xff && b[1] === 0xd8) {
|
|
154
|
+
for (let i = 2; i < b.length - 9;) {
|
|
155
|
+
if (b[i] !== 0xff) { i++; continue; }
|
|
156
|
+
const m = b[i + 1], len = b.readUInt16BE(i + 2);
|
|
157
|
+
if (m >= 0xc0 && m <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(m)) return { w: b.readUInt16BE(i + 7), h: b.readUInt16BE(i + 5) };
|
|
158
|
+
i += 2 + len;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function imageSrc(p, ctx) {
|
|
166
|
+
if (/^(https?:|data:)/.test(p)) return p;
|
|
167
|
+
const f = path.resolve(ctx.baseDir, p);
|
|
168
|
+
if (!fs.existsSync(f)) {
|
|
169
|
+
ctx.warnings?.push(`Imagem "${p}" não encontrada em ${ctx.baseDir}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const ext = path.extname(f).slice(1).toLowerCase().replace("jpg", "jpeg").replace("svg", "svg+xml");
|
|
173
|
+
return `data:image/${ext};base64,${fs.readFileSync(f).toString("base64")}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const isFigure = (el) => el && (el.icon || el.picto || el.diagram || el.chart || el.ufmap || el.svg || el.image || el.qr || el.points);
|
|
177
|
+
|
|
178
|
+
// Caixa de aviso/dica: { aviso: { tipo: importante|atencao|dica|perigo, titulo, texto } }.
|
|
179
|
+
// Atalho: { aviso: "texto" } vira dica sem título. O tipo aceita "atenção" com ou sem acento.
|
|
180
|
+
const AVISO_KINDS = {
|
|
181
|
+
importante: { icon: "sparkles", titulo: "Importante" },
|
|
182
|
+
atencao: { icon: "alert-triangle", titulo: "Atenção" },
|
|
183
|
+
dica: { icon: "lightbulb", titulo: "Dica" },
|
|
184
|
+
perigo: { icon: "x-circle", titulo: "Perigo" },
|
|
185
|
+
};
|
|
186
|
+
export function aviso(e) {
|
|
187
|
+
const a = typeof e.aviso === "string" ? { texto: e.aviso } : e.aviso || {};
|
|
188
|
+
const norm = String(a.tipo || "dica").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
189
|
+
const kind = AVISO_KINDS[norm] ? norm : "dica";
|
|
190
|
+
const { icon, titulo } = AVISO_KINDS[kind];
|
|
191
|
+
// (texto com .t: o ajuste para caber e o fiscal enxergam o aviso; antes ele ficava por cima de outro sem ninguém ver)
|
|
192
|
+
return `<div${attrs(e, `aviso av-${kind}`)}><span class="av-icon">${iconSVG(a.icon || icon, { size: 44, stroke: 2 })}</span><div class="av-body"><div class="av-title t f-heading">${md(a.titulo || titulo)}</div><div class="av-text t f-body">${md(a.texto || "")}</div></div></div>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Renderiza qualquer elemento
|
|
196
|
+
export function el(e, ctx, w, h) {
|
|
197
|
+
if (e == null) return "";
|
|
198
|
+
if (typeof e === "string" || typeof e === "number") return text(String(e), "body");
|
|
199
|
+
if (Array.isArray(e)) return e.map((x) => el(x, ctx, w, h)).join("");
|
|
200
|
+
if((e.web_image||e.web_capture)&&!e.image){const v=e.web_capture||e.web_image;return `<div${attrs(e,'fig fig-pending')}><div class="fp-in"><span class="fp-tag f-label">Referência web pendente</span><span class="fp-text f-body">${esc(typeof v==='string'?v:v.url)}</span></div></div>`;}
|
|
201
|
+
if (e.image_prompt && !e.image && !isFigure(e)) {
|
|
202
|
+
return `<div${attrs(e, "fig fig-pending")}><div class="fp-in"><span class="fp-tag f-label">imagem a gerar</span><span class="fp-text f-body">${esc(String(e.image_prompt).slice(0, 180))}</span></div></div>`;
|
|
203
|
+
}
|
|
204
|
+
if (isFigure(e)) return figureHTML(e, ctx, w, h);
|
|
205
|
+
if (e.textbox) return textbox(e, ctx);
|
|
206
|
+
if (e.table && e.table.cells) return importedTable(e, ctx);
|
|
207
|
+
// tabela de verdade: { table: { head, rows, style, color… } }, { table: [[…], […]] } ou { table: "csv colado" }
|
|
208
|
+
if (e.table) { const t = Array.isArray(e.table) ? { rows: e.table, header: true } : typeof e.table === "string" ? { csv: e.table } : e.table; return `<div${attrs(e, "tbl")}>${tableHTML(t, { theme: ctx?.theme })}</div>`; }
|
|
209
|
+
if (e.drawing) return `<div${attrs(e, "drw")}>${String(e.drawing).replace(/href="media:([^"]+)"/g, (m, p) => `href="${imageSrc(p, ctx) || ""}"`)}</div>`;
|
|
210
|
+
if (e.text != null) return text(e.text, e.as || "body", e);
|
|
211
|
+
if (e.row) return `<div${attrs(e, "row", `gap:${px(e.gap ?? 48)};align-items:${e.valign || "stretch"};justify-content:${e.justify || "flex-start"};`)}>${e.row.map((x) => el(x, ctx)).join("")}</div>`;
|
|
212
|
+
if (e.col) return `<div${attrs(e, "col", `gap:${px(e.gap ?? 28)};justify-content:${e.justify || "flex-start"};align-items:${e.items || "stretch"};`)}>${e.col.map((x) => el(x, ctx)).join("")}</div>`;
|
|
213
|
+
if (e.counter != null) return counter(e);
|
|
214
|
+
if (e.timer != null) return timer(e);
|
|
215
|
+
if (e.poll) return poll(e, ctx);
|
|
216
|
+
if (e.list) return list(e);
|
|
217
|
+
if (e.cards) return cards(e, ctx);
|
|
218
|
+
if (e.stats || e.kpis) return stats(e, ctx);
|
|
219
|
+
if (e.steps || e.process || e.flow) return steps(e, ctx);
|
|
220
|
+
if (e.progress != null) return progress(e);
|
|
221
|
+
if (e.tags || e.chips) return tags(e);
|
|
222
|
+
if (e.rating != null) return rating(e);
|
|
223
|
+
if (e.code) return code(e);
|
|
224
|
+
if (e.shape) return shape(e);
|
|
225
|
+
if (e.badge) return `<div${attrs(e, "badge f-label")}>${md(e.badge)}</div>`;
|
|
226
|
+
if (e.aviso) return aviso(e);
|
|
227
|
+
if (e.video) return video(e, ctx);
|
|
228
|
+
if (e.motion) return `<div${attrs(e, "motion")}>${motionHTML(e.motion,ctx?.theme?.colors)}</div>`;
|
|
229
|
+
if (e.widget) return `<div${attrs(e, "widget")} data-widget="${esc(e.widget)}" data-opts="${esc(JSON.stringify(e))}"></div>`;
|
|
230
|
+
if (e.html) return `<div${attrs(e, "raw")}>${e.html}</div>`;
|
|
231
|
+
if (e.spacer != null) return `<div class="spacer" style="flex:${e.spacer === true ? 1 : 0} 0 ${px(e.spacer === true ? 0 : e.spacer)}"></div>`;
|
|
232
|
+
if (!e.image && Object.keys(e).every((key) => ["image", "fit", "alt", "radius"].includes(key))) return "";
|
|
233
|
+
// atalhos de texto: { h2: "…" }, { label: "…" }, { quote: "…" } …
|
|
234
|
+
for (const r of ["title", "h2", "h3", "lead", "body", "small", "label", "quote", "mono", "hero", "number", "tiny"]) {
|
|
235
|
+
if (e[r] != null && typeof e[r] !== "object") return text(e[r], r, e);
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`Elemento não reconhecido: ${JSON.stringify(e).slice(0, 160)}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ------------------------------------------------------------------------------------------------ texto importado
|
|
241
|
+
// Caixa de texto que veio de um PowerPoint (src/import/pptx.js) com a formatação original: parágrafos com recuo,
|
|
242
|
+
// marcador, alinhamento e espaçamento; trechos com fonte, tamanho, negrito, itálico, sublinhado, cor e equação.
|
|
243
|
+
const FONT_FALLBACK = "Calibri, Carlito, 'Segoe UI', Arial, sans-serif";
|
|
244
|
+
const fontStack = (f) => (f ? `'${String(f).replace(/'/g, "")}', ${FONT_FALLBACK}` : FONT_FALLBACK);
|
|
245
|
+
// marcadores de Wingdings/Symbol desenhados (o caractere do PowerPoint só existe naquela fonte)
|
|
246
|
+
// caractere de fonte de símbolo (Wingdings/Symbol, inclusive na faixa F0xx que o Office grava): desenho ou o
|
|
247
|
+
// equivalente comum; null = não sei (fica o caractere)
|
|
248
|
+
const SYMBOL_FONT = { 0xae: 0x2192, 0xac: 0x2190, 0xad: 0x2191, 0xaf: 0x2193, 0xde: 0x21d2, 0xdc: 0x21d0, 0xdb: 0x21d4, 0xab: 0x2194, 0xb3: 0x2265, 0xa3: 0x2264, 0xb9: 0x2260, 0xb1: 0xb1, 0xb4: 0xd7, 0xb8: 0xf7, 0xb0: 0xb0, 0xd6: 0x221a, 0xa5: 0x221e, 0xbb: 0x2248, 0xb6: 0x2202, 0x44: 0x394, 0x61: 0x3b1, 0x62: 0x3b2, 0x67: 0x3b3, 0x64: 0x3b4, 0x6d: 0x3bc, 0x70: 0x3c0, 0x72: 0x3c1, 0x73: 0x3c3, 0x53: 0x3a3, 0x71: 0x3b8, 0x6c: 0x3bb, 0x77: 0x3c9, 0x57: 0x3a9, 0xe5: 0x2211, 0xf2: 0x222b }; // código do Symbol para o código Unicode
|
|
249
|
+
function symbolGlyph(code, font, c, px) {
|
|
250
|
+
if (code >= 0xf000 && code <= 0xf0ff) code -= 0xf000;
|
|
251
|
+
const f = String(font || "").toLowerCase();
|
|
252
|
+
const s = Math.round(px * 0.62);
|
|
253
|
+
const svg = (inner) => `<svg viewBox="0 0 10 10" width="${s}" height="${s}" style="vertical-align:${-Math.round(s * 0.06)}px" aria-hidden="true">${inner}</svg>`;
|
|
254
|
+
const arrow = (rot) => svg(`<g transform="rotate(${rot} 5 5)"><path d="M0.8 5H8.4M5.6 2L8.8 5L5.6 8" fill="none" stroke="${c}" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></g>`);
|
|
255
|
+
if (f.includes("wingdings")) {
|
|
256
|
+
if (code === 0xd8) return svg(`<path d="M1 1L9.5 5L1 9L3.2 5Z" fill="${c}"/>`);
|
|
257
|
+
if (code === 0xe0 || code === 0xe8) return arrow(0);
|
|
258
|
+
if (code === 0xdf || code === 0xe7) return arrow(180);
|
|
259
|
+
if (code === 0xe1 || code === 0xe9) return arrow(-90);
|
|
260
|
+
if (code === 0xe2 || code === 0xea) return arrow(90);
|
|
261
|
+
if (code === 0xf0) return svg(`<path d="M0.8 3.6H5.4V1.5L9.2 5L5.4 8.5V6.4H0.8Z" fill="none" stroke="${c}" stroke-width="0.9" stroke-linejoin="round"/>`);
|
|
262
|
+
if (code === 0xfc || code === 0xfe) return svg(`<path d="M1.5 5.4L4 8L8.6 2" fill="none" stroke="${c}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>`);
|
|
263
|
+
if (code === 0xfb || code === 0xfd) return svg(`<path d="M2 2L8 8M8 2L2 8" fill="none" stroke="${c}" stroke-width="1.6" stroke-linecap="round"/>`);
|
|
264
|
+
if (code === 0xa7 || code === 0x6e) return svg(`<rect x="2.5" y="2.5" width="5" height="5" fill="${c}"/>`);
|
|
265
|
+
if (code === 0x71 || code === 0x6f || code === 0xa8) return svg(`<rect x="2" y="2" width="6" height="6" fill="none" stroke="${c}" stroke-width="1"/>`);
|
|
266
|
+
if (code === 0x76) return svg(`<path d="M5 0.5L6.4 3.6L9.5 5L6.4 6.4L5 9.5L3.6 6.4L0.5 5L3.6 3.6Z" fill="${c}"/>`);
|
|
267
|
+
if (code === 0x75) return svg(`<path d="M5 0.8L9.2 5L5 9.2L0.8 5Z" fill="${c}"/>`);
|
|
268
|
+
if (code === 0x6c || code === 0x9f || code === 0xa1) return svg(`<circle cx="5" cy="5" r="2.6" fill="${c}"/>`);
|
|
269
|
+
return svg(`<circle cx="5" cy="5" r="2.4" fill="${c}"/>`);
|
|
270
|
+
}
|
|
271
|
+
if (f.includes("symbol")) {
|
|
272
|
+
if (code === 0xb7) return svg(`<circle cx="5" cy="5" r="2.4" fill="${c}"/>`);
|
|
273
|
+
if (SYMBOL_FONT[code]) return `<span style="font-family:'Cambria Math','Segoe UI Symbol',sans-serif;color:${c}">${String.fromCodePoint(SYMBOL_FONT[code])}</span>`;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function bulletHTML(b, size, color) {
|
|
278
|
+
if (b.num) return esc(b.num);
|
|
279
|
+
const ch = String(b.char || ""), c = b.color || color || "currentColor";
|
|
280
|
+
const g = symbolGlyph(ch.codePointAt(0) || 0, b.font, c, size * (b.scale || 1));
|
|
281
|
+
if (g) return g;
|
|
282
|
+
return `<span style="font-family:${esc(fontStack(b.font))};color:${esc(c)}">${esc(ch)}</span>`;
|
|
283
|
+
}
|
|
284
|
+
function runHTML(r) {
|
|
285
|
+
if (r.br) return "<br>";
|
|
286
|
+
if (r.latex != null) return `<span class="tbx-math" style="font-size:${r.size || 36}px">${katex.renderToString(String(r.latex), { throwOnError: false, strict: "ignore", trust: false, maxExpand: 1000 })}</span>`;
|
|
287
|
+
let st = `font-size:${r.sup || r.sub ? Math.round((r.size || 36) * 0.7) : r.size || 36}px;font-family:${fontStack(r.font)};`;
|
|
288
|
+
if (r.b) st += "font-weight:700;";
|
|
289
|
+
if (r.i) st += "font-style:italic;";
|
|
290
|
+
if (r.u || r.s) st += `text-decoration:${[r.u && "underline", r.s && "line-through"].filter(Boolean).join(" ")};`;
|
|
291
|
+
if (r.color) st += `color:${r.color};`;
|
|
292
|
+
if (r.sup) st += "vertical-align:super;";
|
|
293
|
+
if (r.sub) st += "vertical-align:sub;";
|
|
294
|
+
// symPua: só os caracteres da faixa de símbolos (F0xx) são da fonte de símbolo; o resto é texto comum
|
|
295
|
+
const txt = r.sym ? [...String(r.t ?? "")].map((ch) => { const cp = ch.codePointAt(0); return (!r.symPua || (cp >= 0xf000 && cp <= 0xf0ff)) ? symbolGlyph(cp, r.sym, r.color || "currentColor", r.size || 36) || esc(ch) : esc(ch); }).join("") : esc(r.t ?? "");
|
|
296
|
+
const body = `<span style="${esc(st)}">${txt}</span>`;
|
|
297
|
+
return r.link && /^(https?:|mailto:)/i.test(r.link) ? `<a href="${esc(r.link)}" target="_blank" rel="noopener">${body}</a>` : body;
|
|
298
|
+
}
|
|
299
|
+
// altura de linha "simples" de cada fonte (o PowerPoint usa a medida da própria fonte, não um fator fixo)
|
|
300
|
+
const LINE = [[/calibri|carlito/i, 1.22], [/times|tinos|liberation serif/i, 1.15], [/cambria/i, 1.17], [/arial|helvetica|liberation sans|arimo/i, 1.15], [/segoe/i, 1.33], [/georgia/i, 1.14], [/verdana|tahoma/i, 1.21], [/source sans/i, 1.26], [/montserrat/i, 1.22], [/century gothic/i, 1.23]];
|
|
301
|
+
const lineOf = (font) => (LINE.find(([re]) => re.test(font || "")) || [null, 1.2])[1];
|
|
302
|
+
function paragraphHTML(p, idx = 1) {
|
|
303
|
+
const size = p.runs?.find((r) => r.size)?.size || p.size || 36;
|
|
304
|
+
const factor = lineOf(p.runs?.find((r) => r.font)?.font);
|
|
305
|
+
const lh = p.lineHeight == null ? factor : p.lineHeight <= 5 ? +(p.lineHeight * factor).toFixed(3) : `${p.lineHeight}px`;
|
|
306
|
+
let st = `margin:0;font-size:${size}px;line-height:${lh};`;
|
|
307
|
+
if (p.marL) st += `padding-left:${p.marL}px;`;
|
|
308
|
+
if (p.indent) st += `text-indent:${p.indent}px;`;
|
|
309
|
+
if (p.align) st += `text-align:${p.align};`;
|
|
310
|
+
if (p.spaceBefore && idx > 0) st += `margin-top:${p.spaceBefore}px;`; // o PowerPoint ignora o espaço antes do 1º parágrafo
|
|
311
|
+
if (p.spaceAfter) st += `margin-bottom:${p.spaceAfter}px;`;
|
|
312
|
+
if (p.empty) return `<p style="${st}"> </p>`;
|
|
313
|
+
const firstColor = p.runs?.find((r) => r.color)?.color;
|
|
314
|
+
const bu = p.bullet ? `<span class="tbx-bu" style="display:inline-block;text-indent:0;min-width:${Math.max(0, -(p.indent || 0))}px;${(p.indent || 0) >= 0 ? "margin-right:0.4em;" : ""}">${bulletHTML(p.bullet, size, firstColor)}</span>` : "";
|
|
315
|
+
return `<p style="${esc(st)}">${bu}${(p.runs || []).map(runHTML).join("")}</p>`;
|
|
316
|
+
}
|
|
317
|
+
export function textbox(e) {
|
|
318
|
+
const tb = e.textbox || {};
|
|
319
|
+
const [pl, pt, pr, pb] = tb.pad || [0, 0, 0, 0];
|
|
320
|
+
const jc = tb.anchor === "middle" ? "center" : tb.anchor === "bottom" ? "flex-end" : "flex-start";
|
|
321
|
+
let st = `display:flex;flex-direction:column;justify-content:${jc};padding:${pt}px ${pr}px ${pb}px ${pl}px;box-sizing:border-box;`;
|
|
322
|
+
if (tb.nowrap) st += "white-space:nowrap;";
|
|
323
|
+
if (tb.vertical) st += `writing-mode:vertical-rl;${tb.vertical === "up" ? "transform:rotate(180deg);" : ""}`;
|
|
324
|
+
const body = (tb.paragraphs || []).map((p, i) => paragraphHTML(p, i)).join("");
|
|
325
|
+
return `<div${attrs(e, "tbx", st)}>${tb.columns ? `<div style="column-count:${+tb.columns}">${body}</div>` : body}</div>`;
|
|
326
|
+
}
|
|
327
|
+
// tabela importada: colunas e alturas na medida, preenchimento e bordas por célula
|
|
328
|
+
export function importedTable(e) {
|
|
329
|
+
const t = e.table;
|
|
330
|
+
const cols = (t.cols || []).map((w) => `<col style="width:${w}px">`).join("");
|
|
331
|
+
const rows = (t.cells || []).map((row, ri) => `<tr style="height:${t.heights?.[ri] || 0}px">${(row || []).map((c) => {
|
|
332
|
+
if (!c) return "";
|
|
333
|
+
const [pl, pt, pr, pb] = c.pad || [9, 5, 9, 5];
|
|
334
|
+
let st = `padding:${pt}px ${pr}px ${pb}px ${pl}px;vertical-align:${c.anchor === "ctr" ? "middle" : c.anchor === "b" ? "bottom" : "top"};border:${t.border || "1px solid #999"};`;
|
|
335
|
+
if (c.fill) st += `background:${c.fill};`;
|
|
336
|
+
for (const [side, v] of Object.entries(c.borders || {})) st += `border-${side}:${v};`;
|
|
337
|
+
return `<td${c.span > 1 ? ` colspan="${c.span}"` : ""}${c.rowSpan > 1 ? ` rowspan="${c.rowSpan}"` : ""} style="${esc(st)}">${(c.tb?.paragraphs || []).map((p, i) => paragraphHTML(p, i)).join("")}</td>`;
|
|
338
|
+
}).join("")}</tr>`).join("");
|
|
339
|
+
return `<div${attrs(e, "tbx-table")}><table style="border-collapse:collapse;table-layout:fixed;width:100%"><colgroup>${cols}</colgroup>${rows}</table></div>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function counter(e) {
|
|
343
|
+
const v = Number(e.counter);
|
|
344
|
+
const dec = e.decimals ?? (v % 1 ? 1 : 0);
|
|
345
|
+
const shown = v.toLocaleString("pt-BR", { minimumFractionDigits: dec, maximumFractionDigits: dec });
|
|
346
|
+
const size = e.size || SIZES[e.as || "number"];
|
|
347
|
+
return `<div${attrs(e, `t f-display r-number counter pl`, `font-size:${size}px;`)}${e.fit ? " data-fit" : ""} data-to="${v}" data-from="${e.from ?? 0}" data-dec="${dec}" data-prefix="${esc(e.prefix || "")}" data-suffix="${esc(e.suffix || "")}"><span class="cv">${esc(e.prefix || "")}${shown}${esc(e.suffix || "")}</span></div>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function timer(e) {
|
|
351
|
+
const s = Number(e.timer);
|
|
352
|
+
const mm = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
353
|
+
const size = e.size || 220;
|
|
354
|
+
return `<div${attrs(e, "timer pl", `width:${size}px;height:${size}px;`)} data-seconds="${s}" data-auto="${e.auto === false ? 0 : 1}" title="clique para iniciar/pausar">
|
|
355
|
+
<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="44" class="tr-bg"/><circle cx="50" cy="50" r="44" class="tr-fg" pathLength="1" transform="rotate(-90 50 50)"/></svg>
|
|
356
|
+
<div class="tv f-display" style="font-size:${Math.round(size * 0.3)}px">${mm}</div>${e.label ? `<div class="tm-lab f-label">${md(e.label)}</div>` : ""}</div>`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function poll(e, ctx) {
|
|
360
|
+
const opts = e.options || [];
|
|
361
|
+
const letters = "ABCDEFGH";
|
|
362
|
+
let h = `<div${attrs(e, "poll pl")} data-poll="${esc(e.poll)}" ${e.compare ? `data-compare="${esc(e.compare)}"` : ""}>`;
|
|
363
|
+
opts.forEach((o, i) => {
|
|
364
|
+
const label = typeof o === "string" ? o : o.label;
|
|
365
|
+
h += `<div class="po-row" data-i="${i}"><div class="po-key f-display">${letters[i]}</div><div class="po-main"><div class="po-label f-heading">${md(label)}</div><div class="po-track"><div class="po-ghost"></div><div class="po-fill"></div></div></div><div class="po-val f-display" title="clique para digitar o resultado">—</div></div>`;
|
|
366
|
+
});
|
|
367
|
+
h += `<div class="po-hint f-label">${e.hint ? md(e.hint) : "clique no número para digitar o resultado"}</div></div>`;
|
|
368
|
+
return h;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function list(e) {
|
|
372
|
+
const items = e.list;
|
|
373
|
+
const build = e.build;
|
|
374
|
+
const tag = e.numbered ? "ol" : "ul";
|
|
375
|
+
let n = 0;
|
|
376
|
+
const body = items.map((it, i) => {
|
|
377
|
+
const o = typeof it === "string" ? { text: it } : it;
|
|
378
|
+
const step = build ? (e.buildFrom ?? 1) + i : o.step;
|
|
379
|
+
const mark = e.numbered ? `<span class="li-n f-display">${String(++n).padStart(2, "0")}</span>` : `<span class="li-b"></span>`;
|
|
380
|
+
const go = o.goto != null && o.goto !== "" ? `<span class="goto-mark" aria-hidden="true">${iconSVG("arrow-up-right", { size: 30, stroke: 2 })}</span>` : "";
|
|
381
|
+
return `<li${attrs({ ...o, step }, "li")}>${mark}<div class="li-t t f-${o.face || "body"}" style="font-size:${o.size || e.size || SIZES.lead}px">${md(o.text)}${o.sub ? `<div class="li-sub t f-body" style="font-size:${SIZES.small}px">${md(o.sub)}</div>` : ""}</div>${go}</li>`;
|
|
382
|
+
}).join("");
|
|
383
|
+
return `<${tag}${attrs(e, `list ${e.numbered ? "numbered" : ""}`)}>${body}</${tag}>`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function cards(e, ctx) {
|
|
387
|
+
const cols = e.cols || e.cards.length;
|
|
388
|
+
const body = e.cards.map((c, i) => {
|
|
389
|
+
const step = e.build ? (e.buildFrom ?? 1) + i : c.step;
|
|
390
|
+
let top = "";
|
|
391
|
+
if (c.icon) top = `<div class="cd-ico">${iconSVG(c.icon, { size: 64, stroke: 1.8 })}</div>`;
|
|
392
|
+
else if (c.picto) top = `<div class="cd-pic">${picto(c)}</div>`;
|
|
393
|
+
else if (c.number != null) top = `<div class="cd-num t f-display">${md(String(c.number))}</div>`;
|
|
394
|
+
|
|
395
|
+
let badgeHtml = "";
|
|
396
|
+
if (c.badge) badgeHtml = `<span class="cd-badge badge">${md(c.badge)}</span>`;
|
|
397
|
+
else if (c.trend) {
|
|
398
|
+
const isUp = c.trendUp !== false && !String(c.trend).startsWith("-");
|
|
399
|
+
badgeHtml = `<span class="st-trend ${isUp ? 'trend-up' : 'trend-down'}">${iconSVG(isUp ? "trending-up" : "trending-down", { size: 26, stroke: 2.2, cls: "st-trend-ic" })} ${esc(c.trend)}</span>`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let extraWidgets = "";
|
|
403
|
+
if (c.progress != null) extraWidgets += progress({ progress: c.progress, label: c.progressLabel });
|
|
404
|
+
if (c.tags) extraWidgets += tags({ tags: c.tags });
|
|
405
|
+
if (c.rating != null) extraWidgets += rating({ rating: c.rating, label: c.ratingLabel });
|
|
406
|
+
if (c.check === true || c.status === "pro") extraWidgets += `<div class="check-pill pro">${iconSVG("check", { size: 26, stroke: 2.4 })} ${c.checkText ? md(c.checkText) : "Recomendado"}</div>`;
|
|
407
|
+
if (c.check === false || c.status === "con") extraWidgets += `<div class="check-pill con">${iconSVG("x", { size: 26, stroke: 2.4 })} ${c.checkText ? md(c.checkText) : "Evitar"}</div>`;
|
|
408
|
+
|
|
409
|
+
return `<div${attrs({ ...c, step, card: c.hl ? "hi" : true }, "cd")}>
|
|
410
|
+
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;">
|
|
411
|
+
${top}
|
|
412
|
+
${badgeHtml}
|
|
413
|
+
</div>
|
|
414
|
+
${c.title ? `<div class="cd-title t f-heading">${md(c.title)}</div>` : ""}
|
|
415
|
+
${c.text ? `<div class="cd-text t f-body">${md(c.text)}</div>` : ""}
|
|
416
|
+
${extraWidgets}
|
|
417
|
+
${c.foot ? `<div class="cd-foot t f-label">${md(c.foot)}</div>` : ""}
|
|
418
|
+
${c.goto != null && c.goto !== "" ? `<span class="goto-mark" aria-hidden="true">${iconSVG("arrow-up-right", { size: 30, stroke: 2 })}</span>` : ""}
|
|
419
|
+
</div>`;
|
|
420
|
+
}).join("");
|
|
421
|
+
return `<div${attrs(e, "cards", `grid-template-columns:repeat(${cols},1fr);`)}>${body}</div>`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function stats(e, ctx) {
|
|
425
|
+
const items = e.stats || e.kpis || [];
|
|
426
|
+
const cols = e.cols || Math.min(items.length, 4) || 3;
|
|
427
|
+
const body = items.map((st, i) => {
|
|
428
|
+
const step = e.build ? (e.buildFrom ?? 1) + i : st.step;
|
|
429
|
+
const valColor = st.color ? `color:${textColorVal(st.color)};` : "";
|
|
430
|
+
let iconHtml = "";
|
|
431
|
+
if (st.icon) {
|
|
432
|
+
iconHtml = `<div class="st-icon">${iconSVG(st.icon, { size: 48, stroke: 1.8 })}</div>`;
|
|
433
|
+
}
|
|
434
|
+
let trendHtml = "";
|
|
435
|
+
if (st.trend) {
|
|
436
|
+
const isUp = st.trendUp !== false && !String(st.trend).startsWith("-");
|
|
437
|
+
trendHtml = `<div class="st-trend ${isUp ? 'trend-up' : 'trend-down'}">${iconSVG(isUp ? "trending-up" : "trending-down", { size: 26, stroke: 2.2, cls: "st-trend-ic" })} ${esc(st.trend)}</div>`;
|
|
438
|
+
}
|
|
439
|
+
return `<div${attrs({ ...st, step, card: st.card ?? true }, "stat-card")}>
|
|
440
|
+
<div class="st-top">
|
|
441
|
+
${iconHtml}
|
|
442
|
+
${trendHtml}
|
|
443
|
+
</div>
|
|
444
|
+
<div class="st-val t f-display" style="${valColor}">${esc(String(st.value ?? st.stat ?? ""))}</div>
|
|
445
|
+
${st.label ? `<div class="st-lab t f-heading">${md(st.label)}</div>` : ""}
|
|
446
|
+
${st.text ? `<div class="st-sub t f-body">${md(st.text)}</div>` : ""}
|
|
447
|
+
</div>`;
|
|
448
|
+
}).join("");
|
|
449
|
+
return `<div${attrs(e, "stats-grid", `grid-template-columns:repeat(${cols},1fr);`)}>${body}</div>`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function steps(e, ctx) {
|
|
453
|
+
const items = e.steps || e.process || e.flow || [];
|
|
454
|
+
const cols = e.cols || items.length || 3;
|
|
455
|
+
const body = items.map((st, i) => {
|
|
456
|
+
const step = e.build ? (e.buildFrom ?? 1) + i : st.step;
|
|
457
|
+
const num = st.stepNum ?? (i + 1);
|
|
458
|
+
let iconHtml = "";
|
|
459
|
+
if (st.icon) {
|
|
460
|
+
iconHtml = `<div class="step-icon">${iconSVG(st.icon, { size: 42, stroke: 1.8 })}</div>`;
|
|
461
|
+
}
|
|
462
|
+
return `<div${attrs({ ...st, step, card: st.card ?? true }, "step-card")}>
|
|
463
|
+
<div class="step-header">
|
|
464
|
+
<span class="step-num t f-display">${String(num).padStart(2, "0")}</span>
|
|
465
|
+
${iconHtml}
|
|
466
|
+
</div>
|
|
467
|
+
${st.title ? `<div class="step-title t f-heading">${md(st.title)}</div>` : ""}
|
|
468
|
+
${st.text ? `<div class="step-text t f-body">${md(st.text)}</div>` : ""}
|
|
469
|
+
${st.tag ? `<div class="tag-pill" style="align-self:flex-start;margin-top:auto;">${md(st.tag)}</div>` : ""}
|
|
470
|
+
${st.goto != null && st.goto !== "" ? `<span class="goto-mark" aria-hidden="true">${iconSVG("arrow-up-right", { size: 30, stroke: 2 })}</span>` : ""}
|
|
471
|
+
${i < items.length - 1 ? `<div class="step-connector" aria-hidden="true">${iconSVG("arrow-right", { size: 40, stroke: 2 })}</div>` : ""}
|
|
472
|
+
</div>`;
|
|
473
|
+
}).join("");
|
|
474
|
+
return `<div${attrs(e, "steps-flow", `grid-template-columns:repeat(${cols},1fr);`)}>${body}</div>`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function progress(e) {
|
|
478
|
+
const val = Math.min(100, Math.max(0, Number(e.progress ?? e.value ?? 0)));
|
|
479
|
+
const label = e.label || "";
|
|
480
|
+
const color = e.color ? colorVal(e.color) : "var(--hi)";
|
|
481
|
+
return `<div${attrs(e, "prog-widget")}>
|
|
482
|
+
<div class="prog-top">
|
|
483
|
+
${label ? `<span class="prog-label t f-heading">${md(label)}</span>` : ""}
|
|
484
|
+
<span class="prog-val t f-display">${val}%</span>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="prog-track">
|
|
487
|
+
<div class="prog-fill" style="width:${val}%;background:${color};"></div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function tags(e) {
|
|
493
|
+
const list = e.tags || e.chips || [];
|
|
494
|
+
return `<div${attrs(e, "tags-cloud")}>${list.map((t) => `<span class="tag-pill">${md(typeof t === "string" ? t : t.text)}</span>`).join("")}</div>`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function rating(e) {
|
|
498
|
+
const stars = Math.min(5, Math.max(1, Math.round(Number(e.rating || 5))));
|
|
499
|
+
const score = e.score || `${stars}.0/5`;
|
|
500
|
+
return `<div${attrs(e, "rating-widget")}>
|
|
501
|
+
<div class="rating-stars">${Array.from({ length: 5 }, (_, k) => iconSVG("star", { size: 30, stroke: 1.8, cls: k < stars ? "star-on" : "star-off" })).join("")}</div>
|
|
502
|
+
${score ? `<span class="rating-score t f-display">${esc(score)}</span>` : ""}
|
|
503
|
+
${e.label ? `<span class="rating-label t f-body">${md(e.label)}</span>` : ""}
|
|
504
|
+
</div>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function code(e) {
|
|
508
|
+
const source = String(e.code).replace(/\s+$/, "");
|
|
509
|
+
const language = resolveCodeLanguage(e.language, e.filename);
|
|
510
|
+
const lines = language ? highlightCode(source, language) : source.split("\n").map(esc);
|
|
511
|
+
const hl = new Set([].concat(e.highlight || []));
|
|
512
|
+
const body = lines.map((l, i) => `<div class="cl${hl.has(i + 1) ? " hl" : ""}"><span class="cn">${i + 1}</span><span class="cc">${l || " "}</span></div>`).join("");
|
|
513
|
+
return `<div${attrs(e, "code f-mono", `font-size:${e.size || 34}px;`)}${language ? ` data-language="${esc(language)}"` : ""}>${body}</div>`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Formas desenhadas (caixa 100×100 esticada no w×h): preenchimento e contorno de verdade, não só um retângulo
|
|
517
|
+
const SHAPE_PATHS = {
|
|
518
|
+
triangle: "50,0 100,100 0,100",
|
|
519
|
+
diamond: "50,0 100,50 50,100 0,50",
|
|
520
|
+
hexagon: "25,0 75,0 100,50 75,100 25,100 0,50",
|
|
521
|
+
star: "50,0 61,35 98,35 68,57 79,91 50,70 21,91 32,57 2,35 39,35",
|
|
522
|
+
arrow: "0,30 62,30 62,5 100,50 62,95 62,70 0,70",
|
|
523
|
+
chevron: "0,0 70,0 100,50 70,100 0,100 30,50",
|
|
524
|
+
bubble: "M8,0 H92 Q100,0 100,8 V64 Q100,72 92,72 H40 L22,100 L26,72 H8 Q0,72 0,64 V8 Q0,0 8,0 Z",
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export function shape(e) {
|
|
528
|
+
const k = e.shape;
|
|
529
|
+
if (SHAPE_PATHS[k]) {
|
|
530
|
+
const fill = colorVal(e.fill || e.bg) || "var(--surface)";
|
|
531
|
+
const stroke = colorVal(e.stroke);
|
|
532
|
+
// --shape-fill: o Preenchimento do Studio (visualEdits.fill) troca a cor sem mexer no desenho
|
|
533
|
+
// --shape-fill / --shape-stroke / --shape-sw: o inspetor do Studio troca cor e contorno sem mexer no desenho
|
|
534
|
+
const paint = `fill="${fill}"${stroke ? ` stroke="${stroke}" stroke-width="${e.strokeWidth || 4}"` : ""} vector-effect="non-scaling-stroke" stroke-linejoin="round" style="fill:var(--shape-fill,${fill});stroke:var(--shape-stroke,${stroke || "none"});stroke-width:var(--shape-sw,${e.strokeWidth || 4}px)"`;
|
|
535
|
+
const d = SHAPE_PATHS[k];
|
|
536
|
+
const svg = `<svg class="shape-svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">${d.startsWith("M") ? `<path d="${d}" ${paint}/>` : `<polygon points="${d}" ${paint}/>`}</svg>`;
|
|
537
|
+
return `<div${attrs({ ...e, bg: undefined }, `shape shape-${k}`)}>${svg}${e.content ? `<div class="shape-content">${el(e.content)}</div>` : ""}</div>`;
|
|
538
|
+
}
|
|
539
|
+
const fill = colorVal(e.fill) || (k === "line" ? null : "var(--surface)");
|
|
540
|
+
const stroke = colorVal(e.stroke);
|
|
541
|
+
let st = "";
|
|
542
|
+
if (fill) st += `background:${fill};`;
|
|
543
|
+
if (stroke) st += `border:${e.strokeWidth || 4}px solid ${stroke};`;
|
|
544
|
+
if (k === "circle") st += "border-radius:50%;";
|
|
545
|
+
if (k === "pill") st += "border-radius:999px;";
|
|
546
|
+
if (k === "rounded") st += `border-radius:${e.radius ?? "var(--radius)"};`;
|
|
547
|
+
if (k === "line") st += `background:${colorVal(e.color || e.stroke) || "var(--fg)"};height:${px(e.thickness || 4)};`;
|
|
548
|
+
return `<div${attrs(e, `shape shape-${k}`, st)}>${e.content ? el(e.content) : ""}</div>`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function video(e, ctx) {
|
|
552
|
+
const player=videoPlayer(e,ctx);if(player)return player.replace(/^<div class="([^"]+)"/,(_,cls)=>`<div${attrs(e,cls)}`);
|
|
553
|
+
return `<a${attrs(e, "video")} href="${esc(e.video)}" target="_blank" rel="noopener"><span class="vd-play"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></span><span class="vd-label t f-heading">${md(e.label || "Assistir")}</span><span class="vd-url t f-label">${esc(e.video.replace(/^https?:\/\/(www\.)?/, "").slice(0, 60))}</span></a>`;
|
|
554
|
+
}
|