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
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
SagaDeck Studio — editor visual do slide (painel "Formatar").
|
|
3
|
+
Um formulário por layout (campos em src/layouts.js e src/elements.js), com listas editáveis,
|
|
4
|
+
editor de elementos (figuras, blocos livres) e, para qualquer campo que o formulário não
|
|
5
|
+
conheça, um editor genérico em "Outros campos" — assim tudo o que existe no YAML é editável.
|
|
6
|
+
|
|
7
|
+
API: SlideForm.render(container, slide, ctx)
|
|
8
|
+
ctx.commit(structural) o slide mudou; structural = mudou a estrutura (reconstruir o formulário)
|
|
9
|
+
ctx.pickIcon(cb) abre a biblioteca de ícones e chama cb(nome)
|
|
10
|
+
ctx.layoutLabel(nome) nome do layout em português
|
|
11
|
+
========================================================================== */
|
|
12
|
+
(function () {
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------------------------
|
|
16
|
+
// Vocabulário
|
|
17
|
+
// ---------------------------------------------------------------------------------------------
|
|
18
|
+
const TEXT_ROLES = [
|
|
19
|
+
["hero", "Enorme"], ["title", "Título grande"], ["h2", "Título"], ["h3", "Subtítulo"], ["lead", "Destaque"],
|
|
20
|
+
["body", "Corpo"], ["small", "Pequeno"], ["label", "Rótulo"], ["quote", "Citação"], ["number", "Número"],
|
|
21
|
+
["mono", "Monoespaçado"], ["tiny", "Mínimo"],
|
|
22
|
+
];
|
|
23
|
+
const COLOR_ROLES = ["fg", "hi", "em", "muted", "line", "bg"];
|
|
24
|
+
const POSES = [["stand", "Em pé"], ["walk", "Andando"], ["run", "Correndo"], ["sit", "Sentado"], ["drive", "Dirigindo"],
|
|
25
|
+
["phone", "No telefone"], ["watch", "Olhando"], ["point", "Apontando"], ["raise", "Mão levantada"], ["shrug", "Braços abertos"],
|
|
26
|
+
["think", "Pensando"], ["stamp", "Carimbando"], ["cheer", "Comemorando"], ["sleep", "Dormindo"]];
|
|
27
|
+
const CAR_SEAT = [["human", "Pessoa"], ["human-watch", "Pessoa observando"], ["human-phone", "Pessoa no celular"], ["machine", "Máquina"], ["none", "Vazio"]];
|
|
28
|
+
const ANIMS = [["", "Padrão (subir)"], ["fade", "Aparecer"], ["pop", "Saltar"], ["left", "Da esquerda"], ["right", "Da direita"], ["down", "De cima"], ["zoom", "Zoom"], ["none", "Sem animação"]];
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------------------------
|
|
31
|
+
// Construtores de esquema
|
|
32
|
+
// ---------------------------------------------------------------------------------------------
|
|
33
|
+
const f = {
|
|
34
|
+
text: (k, label, o = {}) => ({ k, label, type: "text", ...o }),
|
|
35
|
+
area: (k, label, o = {}) => ({ k, label, type: "textarea", ...o }),
|
|
36
|
+
num: (k, label, o = {}) => ({ k, label, type: "number", ...o }),
|
|
37
|
+
bool: (k, label, o = {}) => ({ k, label, type: "bool", ...o }),
|
|
38
|
+
select: (k, label, options, o = {}) => ({ k, label, type: "select", options, ...o }),
|
|
39
|
+
codeLanguage: (k, label, filenameKey) => ({ k, label, type: "codeLanguage", filenameKey }),
|
|
40
|
+
color: (k, label, o = {}) => ({ k, label, type: "color", ...o }),
|
|
41
|
+
icon: (k, label, o = {}) => ({ k, label, type: "icon", ...o }),
|
|
42
|
+
nums: (k, label, o = {}) => ({ k, label, type: "nums", ...o }), // "2, 3" <-> [2, 3]
|
|
43
|
+
pair: (k, label, labels, o = {}) => ({ k, label, type: "pair", labels, ...o }), // [a, b]
|
|
44
|
+
list: (k, label, item, o = {}) => ({ k, label, type: "list", item, ...o }),
|
|
45
|
+
obj: (k, label, fields, o = {}) => ({ k, label, type: "object", fields, ...o }),
|
|
46
|
+
el: (k, label, o = {}) => ({ k, label, type: "element", ...o }),
|
|
47
|
+
els: (k, label, o = {}) => ({ k, label, type: "elements", ...o }),
|
|
48
|
+
chart: (k, label, o = {}) => ({ k, label, type: "chart", ...o }),
|
|
49
|
+
plot: (k, label, o = {}) => ({ k, label, type: "plot", ...o }),
|
|
50
|
+
photo: (k, label, o = {}) => ({ k, label, type: "photo", ...o }), // foto da pessoa (arquivo escolhido) ou vazia = de demonstração
|
|
51
|
+
lines: (k, label, o = {}) => ({ k, label, type: "linemap", ...o }), // {2: "texto", 5: null} <-> "2: texto" por linha
|
|
52
|
+
tablegrid: (label, o = {}) => ({ k: ["head", "rows"], label, type: "tablegrid", ...o }), // tabela de texto (layout table)
|
|
53
|
+
sheet: (label, o = {}) => ({ k: ["data", "labels", "series", "parts"], label, type: "sheet", ...o }), // planilha dos dados do gráfico // fórmulas, pontos e superfície (slide science)
|
|
54
|
+
json: (k, label, o = {}) => ({ k, label, type: "json", ...o }), // objeto editado como JSON validado
|
|
55
|
+
more: (fields) => ({ type: "more", fields }),
|
|
56
|
+
action: (label, run, o = {}) => ({ type: "action", label, run, ...o }),
|
|
57
|
+
};
|
|
58
|
+
// item de lista: texto simples | campos de objeto | texto-ou-objeto (string vira {text}) | elemento
|
|
59
|
+
const T = "text";
|
|
60
|
+
const TA = "textarea";
|
|
61
|
+
const obj = (fields, o = {}) => ({ fields, ...o });
|
|
62
|
+
const textOrObj = (fields, o = {}) => ({ fields, textOrObj: true, ...o });
|
|
63
|
+
|
|
64
|
+
// dinâmicas a dois: quem fala/age e o roteiro do Git (o simulador calcula diff, push recusado, merge e conflito)
|
|
65
|
+
const WHO = f.select("who", "Quem", [["1", "Pessoa 1"], ["2", "Pessoa 2"]], { empty: false, default: "1", parse: Number });
|
|
66
|
+
const DUEL_FIELDS = [f.pair("people", "As duas pessoas", ["Ana", "Beto"]), f.text("file", "Arquivo", { placeholder: "soma.js" }), f.area("base", "Arquivo no começo (igual para os dois)", { mono: true, rows: 5 }),
|
|
67
|
+
f.list("turns", "Turnos (um por clique)", obj([WHO, f.lines("edit", "Linhas que muda", { hint: "Uma por linha: 2: novo texto (2: (apagar) apaga)" }), f.text("commit", "Commit (mensagem)"),
|
|
68
|
+
f.bool("push", "git push"), f.bool("pull", "git pull"), f.lines("resolve", "Resolve o conflito", { hint: "ours (fica a sua), theirs (a do remoto), both, ou as linhas: 2: texto final" }),
|
|
69
|
+
f.bool("log", "git log --graph"), f.more([f.text("say", "Explicação no lugar da automática"), f.text("note", "Nota embaixo")])]),
|
|
70
|
+
{ addLabel: "Adicionar turno", newItem: () => ({ who: 1, commit: "" }) })];
|
|
71
|
+
const CARD = [f.icon("icon", "Ícone"), f.text("title", "Título"), f.area("text", "Texto"),
|
|
72
|
+
f.more([f.text("number", "Número"), f.text("foot", "Rodapé"), f.text("badge", "Selo"), f.bool("hl", "Destacar"), f.num("step", "Aparece no clique"), f.text("goto", "Ao clicar, ir para", { hint: "Id do slide de destino (ou o número dele): navegação por caminhos." })])];
|
|
73
|
+
const STAT = [f.text("value", "Valor"), f.text("label", "Rótulo"), f.text("trend", "Tendência"), f.icon("icon", "Ícone"),
|
|
74
|
+
f.more([f.bool("trendUp", "Tendência positiva"), f.color("color", "Cor")])];
|
|
75
|
+
const STEP = [f.text("title", "Título"), f.area("text", "Texto"), f.icon("icon", "Ícone"), f.more([f.text("tag", "Etiqueta"), f.text("goto", "Ao clicar, ir para", { hint: "Id do slide de destino (ou o número dele): navegação por caminhos." })])];
|
|
76
|
+
// bloco do one-page (problema, solução): texto, números grandes e tópicos
|
|
77
|
+
const OP_BLOCK = [f.text("title", "Título"), f.area("text", "Texto"),
|
|
78
|
+
f.list("numbers", "Números", obj([f.text("value", "Valor"), f.text("label", "Rótulo")]), { addLabel: "Adicionar número", newItem: () => ({ value: "10%", label: "" }) }),
|
|
79
|
+
f.list("items", "Tópicos", T, { addLabel: "Adicionar tópico" })];
|
|
80
|
+
// item do status semanal: texto, com responsável e prazo opcionais
|
|
81
|
+
const STATUS_ITEM = textOrObj([f.area("text", "Texto"), f.text("owner", "Responsável"), f.text("due", "Prazo")]);
|
|
82
|
+
// opção do mapa de caminhos (layout hub): leva à seção do caminho
|
|
83
|
+
const HUB_OPTION = [f.icon("icon", "Ícone"), f.text("title", "Título"), f.area("text", "Texto"), f.text("meta", "Detalhe", { hint: "Ex.: 30 dias, prorrogável" }), f.text("goto", "Ao clicar, ir para", { hint: "Id do slide de destino (ou o número dele): navegação por caminhos." })];
|
|
84
|
+
const TIMELINE_EV = [f.text("when", "Quando"), f.text("title", "Título"), f.area("text", "Texto"), f.text("tag", "Etiqueta")];
|
|
85
|
+
const COMPARE_SIDE = [f.text("label", "Rótulo"), f.text("value", "Valor grande"), f.text("title", "Título"), f.area("text", "Texto"),
|
|
86
|
+
f.list("items", "Itens", T), f.el("figure", "Figura"), f.bool("hl", "Destacar")];
|
|
87
|
+
const MATRIX_CELL = [f.text("title", "Título"), f.area("text", "Texto"), f.text("example", "Exemplo"), f.icon("icon", "Ícone"), f.bool("hl", "Destacar")];
|
|
88
|
+
const COMMON_MORE = [f.text("id", "Id do slide", { hint: "Nome curto para os links (goto, Voltar) chegarem aqui. Ex.: mapa, dev" }),
|
|
89
|
+
f.text("back", "Botão Voltar para", { hint: "Id do slide (ex.: mapa): mostra 'Voltar' no canto" }),
|
|
90
|
+
f.text("next", "No fim, avançar para", { hint: "Id do slide: o fim de um caminho volta ao mapa em vez de seguir a ordem" }),
|
|
91
|
+
f.select("markStyle", "Estilo do ==destaque== neste slide", [["marca-texto", "Marca-texto"], ["sublinhado", "Sublinhado"], ["cor", "Só cor"], ["negrito", "Negrito colorido"], ["nenhum", "Sem destaque"]], { empty: "O do deck" }),
|
|
92
|
+
f.num("titleSize", "Tamanho do título (px)"), f.bool("fit", "Encolher o título para caber"), f.text("source", "Fonte (rodapé)"), f.text("transition", "Transição", { datalist: ["fade", "cut", "saida", "morph"] }), f.text("ambient", "Ambiente de fundo", { datalist: ["pontos", "grade"] }),
|
|
93
|
+
f.els("add", "Elementos extras no fim"), f.el("background", "Figura de fundo")];
|
|
94
|
+
|
|
95
|
+
// Campos de cada layout (o que src/layouts.js lê). "more" = recolhido em "Mais opções".
|
|
96
|
+
// grades adaptáveis (src/adaptive-layouts.js): os mesmos campos nas três
|
|
97
|
+
const ADAPTIVE = [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("items", "Itens", obj([f.text("title", "Título"), f.area("text", "Texto"), f.text("value", "Valor"), f.icon("icon", "Ícone"), f.area("code", "Código (opcional)", { mono: true, rows: 4 }), f.text("foot", "Nota")]), { addLabel: "Adicionar item", newItem: () => ({ title: "Novo item", text: "" }), max: 12 }), f.bool("build", "Revelar por clique")];
|
|
98
|
+
const CODE_SOURCE_FIELDS = [
|
|
99
|
+
f.text("filename", "Nome do arquivo", { placeholder: "exemplo.py" }),
|
|
100
|
+
f.codeLanguage("language", "Linguagem", "filename"),
|
|
101
|
+
f.area("code", "Código", { mono: true, rows: 10, hint: "O código é exibido, sem execução." }),
|
|
102
|
+
f.nums("highlight", "Linhas destacadas (a partir de 1)"),
|
|
103
|
+
];
|
|
104
|
+
const CODE_SIZE_FIELDS = [f.num("size", "Tamanho do código (px)")];
|
|
105
|
+
const LAYOUTS = {
|
|
106
|
+
mosaic: ADAPTIVE, ribbon: ADAPTIVE, dossier: ADAPTIVE,
|
|
107
|
+
cover: [f.text("kicker", "Chapéu"), f.area("title", "Título"), f.text("subtitle", "Subtítulo"), f.text("author", "Autor"), f.text("role", "Cargo do autor"),
|
|
108
|
+
f.el("figure", "Figura"), f.more([f.num("titleSize", "Tamanho do título (px)")])],
|
|
109
|
+
section: [f.text("number", "Número"), f.text("kicker", "Chapéu"), f.area("title", "Título"), f.text("subtitle", "Subtítulo"), f.el("figure", "Figura"),
|
|
110
|
+
f.more([f.num("titleSize", "Tamanho do título (px)")])],
|
|
111
|
+
definition: [f.text("kicker", "Chapéu"), f.text("term", "Termo"), f.text("origin", "Origem", { hint: "Ex.: do grego, do latim" }),
|
|
112
|
+
f.list("parts", "Partes da palavra", obj([f.text("word", "Parte"), f.text("meaning", "Significado")]), { addLabel: "Adicionar parte", newItem: () => ({ word: "", meaning: "" }) }),
|
|
113
|
+
f.area("text", "Definição"), f.icon("icon", "Ícone"), f.el("figure", "Figura (no lugar do ícone)"), f.more([f.bool("build", "Revelar por cliques"), f.num("termSize", "Tamanho do termo (px)")])],
|
|
114
|
+
calendar: [f.text("kicker", "Chapéu"), f.area("title", "Título"),
|
|
115
|
+
f.list("events", "Eventos", obj([f.text("date", "Data", { hint: "2026-12-18, 18/12/2026 ou só o mês: 2026-12" }), f.text("title", "Título"), f.text("tag", "Tipo", { hint: "Filme, Série… (cada tipo, uma cor)" }), f.area("text", "Detalhe")]), { addLabel: "Adicionar evento", newItem: () => ({ date: "", title: "" }) }),
|
|
116
|
+
f.list("undated", "Sem data", obj([f.text("title", "Título"), f.area("text", "Detalhe")]), { addLabel: "Adicionar", newItem: () => ({ title: "" }) }),
|
|
117
|
+
f.more([f.num("cols", "Colunas"), f.text("undatedTitle", "Título do cartão sem data"), f.bool("build", "Revelar por cliques")])],
|
|
118
|
+
poster: [f.text("kicker", "Chapéu"), f.area("title", "Título"), f.text("subtitle", "Subtítulo"), f.el("hero", "Ilustração principal"),
|
|
119
|
+
f.list("panels", "Painéis", obj([f.text("label", "Letra"), f.text("title", "Título"), f.area("text", "Texto"), f.icon("icon", "Ícone"), f.el("figure", "Figura (no lugar do ícone)"),
|
|
120
|
+
f.list("facts", "Números", obj([f.text("value", "Valor"), f.text("label", "Legenda")]), { addLabel: "Adicionar número", newItem: () => ({ value: "", label: "" }) })]),
|
|
121
|
+
{ addLabel: "Adicionar painel", newItem: () => ({ title: "", text: "", icon: "circle" }) }),
|
|
122
|
+
f.area("key", "Nota de rodapé"), f.more([f.bool("flow", "Setas entre os painéis"), f.num("cols", "Colunas"), f.bool("build", "Revelar por cliques")])],
|
|
123
|
+
statement: [f.text("kicker", "Chapéu"), f.area("text", "Frase", { hint: "Ou use Linhas para revelar uma frase por clique." }),
|
|
124
|
+
f.list("lines", "Linhas (uma por clique)", textOrObj([f.area("text", "Texto"), f.select("as", "Estilo", TEXT_ROLES, { empty: "Título grande" }), f.color("color", "Cor"), f.num("step", "Clique")]), { addLabel: "Adicionar linha" }),
|
|
125
|
+
f.text("by", "Assinatura"),
|
|
126
|
+
f.more([f.bool("center", "Centralizar"), f.select("as", "Estilo da frase", TEXT_ROLES, { empty: "Título grande" }), f.num("size", "Tamanho (px)"), f.num("byStep", "Assinatura no clique")])],
|
|
127
|
+
hub: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.text("question", "Pergunta"),
|
|
128
|
+
f.list(["options", "items"], "Caminhos", obj(HUB_OPTION), { addLabel: "Adicionar caminho", newItem: () => ({ icon: "signpost", title: "Novo caminho", text: "", goto: "" }) }),
|
|
129
|
+
f.more([f.num("cols", "Colunas"), f.bool("build", "Um caminho por clique")])],
|
|
130
|
+
onepage: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.text("subtitle", "Subtítulo"),
|
|
131
|
+
f.list("journey", "Jornada", textOrObj([f.icon("icon", "Ícone"), f.text("title", "Título"), f.text("text", "Mini-frase"), f.text("goto", "Ao clicar, ir para")]),
|
|
132
|
+
{ addLabel: "Adicionar etapa da jornada", newItem: () => ({ icon: "circle", title: "Etapa", text: "" }) }),
|
|
133
|
+
f.obj("problem", "O problema", OP_BLOCK, { stringAs: "text" }), f.obj("solution", "A solução", OP_BLOCK, { stringAs: "text" }),
|
|
134
|
+
f.obj("dashboard", "Painel", [f.list("numbers", "Números grandes", obj([f.text("value", "Valor"), f.text("label", "Rótulo"), f.text("trend", "Tendência", { hint: "Ex.: +12% (com - na frente fica vermelho)" }), f.text("title", "Título")]),
|
|
135
|
+
{ addLabel: "Adicionar número", newItem: () => ({ value: "100", label: "Indicador" }) }),
|
|
136
|
+
f.list("figures", "Gráficos e mapa", "element", { addLabel: "Adicionar gráfico ou mapa", newItem: () => ({ title: "Gráfico", chart: "bar", data: [{ label: "A", value: 10 }, { label: "B", value: 20 }] }) })]),
|
|
137
|
+
f.more([f.text("journeyTitle", "Título da jornada", { placeholder: "Jornada" })])],
|
|
138
|
+
status: [f.text("kicker", "Chapéu", { hint: "Ex.: Semana 39 · 22 a 26/09" }), f.text("title", "Título"),
|
|
139
|
+
f.select("health", "Saúde", [["ok", "Em dia"], ["risco", "Atenção"], ["atrasado", "Atrasado"]], { empty: "Sem indicador" }), f.num("progress", "Avanço (%)"),
|
|
140
|
+
f.text("highlight", "Destaque da semana"),
|
|
141
|
+
f.list("done", "Feito", STATUS_ITEM, { addLabel: "Adicionar feito" }), f.list("doing", "Em andamento", STATUS_ITEM, { addLabel: "Adicionar em andamento" }),
|
|
142
|
+
f.list("blocked", "Bloqueios", STATUS_ITEM, { addLabel: "Adicionar bloqueio" }), f.list("risks", "Riscos e problemas", STATUS_ITEM, { addLabel: "Adicionar risco" }),
|
|
143
|
+
f.list("upcoming", "Próximos passos", STATUS_ITEM, { addLabel: "Adicionar próximo passo" }),
|
|
144
|
+
f.list("shots", "Telas da semana (até 3)", obj([f.text("image", "Imagem (arquivo ou link)"), f.text("caption", "Legenda")]), { addLabel: "Adicionar tela", max: 3, newItem: () => ({ image: "", caption: "" }) }),
|
|
145
|
+
f.more([f.text("healthLabel", "Texto da saúde", { hint: "Troca o rótulo, ex.: Atrasado 1 semana" })])],
|
|
146
|
+
quote: [f.text("kicker", "Chapéu"), f.area("quote", "Citação"), f.text("by", "Autor"), f.text("role", "Cargo"), f.text("after", "Comentário (depois de um clique)"),
|
|
147
|
+
f.more([f.num("afterStep", "Comentário no clique"), f.num("size", "Tamanho (px)")])],
|
|
148
|
+
number: [f.text("kicker", "Chapéu"), f.num("value", "Valor"), f.text("prefix", "Antes do número"), f.text("suffix", "Depois do número"), f.text("label", "Rótulo"),
|
|
149
|
+
f.area("context", "Contexto"), f.el("side", "Ao lado"),
|
|
150
|
+
f.more([f.num("decimals", "Casas decimais"), f.num("from", "Contar a partir de"), f.color("valueColor", "Cor do número"), f.num("size", "Tamanho (px)"),
|
|
151
|
+
f.num("labelSize", "Tamanho do rótulo (px)"), f.num("contextStep", "Contexto no clique"), f.num("sideStep", "Lado no clique")])],
|
|
152
|
+
split: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.area("body", "Texto"), f.list("bullets", "Tópicos", T, { addLabel: "Adicionar tópico" }),
|
|
153
|
+
f.el("figure", "Figura"), f.els("content", "Blocos abaixo do texto"),
|
|
154
|
+
f.more([f.text("ratio", "Proporção texto:figura", { placeholder: "1:1" }), f.bool("reverse", "Figura à esquerda"), f.bool("build", "Tópicos um por clique"),
|
|
155
|
+
f.select("bodyAs", "Estilo do texto", TEXT_ROLES, { empty: "Destaque" }), f.num("bulletSize", "Tamanho dos tópicos (px)"),
|
|
156
|
+
f.select("titleAs", "Estilo do título", TEXT_ROLES, { empty: "Título" }), f.num("figureStep", "Figura no clique")])],
|
|
157
|
+
cards: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("items", "Cartões", obj(CARD), { addLabel: "Adicionar cartão", newItem: () => ({ icon: "star", title: "Novo cartão", text: "" }) }),
|
|
158
|
+
f.more([f.num("cols", "Colunas"), f.bool("build", "Um por clique")])],
|
|
159
|
+
stats: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list(["stats", "kpis", "items"], "Indicadores", obj(STAT), { addLabel: "Adicionar indicador", newItem: () => ({ value: "100%", label: "Indicador" }) }),
|
|
160
|
+
f.more([f.num("cols", "Colunas"), f.bool("build", "Um por clique")])],
|
|
161
|
+
steps: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list(["steps", "process", "flow", "items"], "Etapas", obj(STEP), { addLabel: "Adicionar etapa", newItem: () => ({ title: "Nova etapa", text: "" }) }),
|
|
162
|
+
f.more([f.num("cols", "Colunas"), f.bool("build", "Uma por clique")])],
|
|
163
|
+
list: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("items", "Itens", textOrObj([f.area("text", "Texto"), f.text("sub", "Detalhe"), f.text("goto", "Ao clicar, ir para", { hint: "Id do slide de destino (ou o número dele): navegação por caminhos." })]), { addLabel: "Adicionar item" }),
|
|
164
|
+
f.more([f.bool("numbered", "Numerada", { default: true }), f.bool("build", "Um por clique"), f.num("size", "Tamanho (px)")])],
|
|
165
|
+
timeline: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("events", "Eventos", obj(TIMELINE_EV), { addLabel: "Adicionar evento", newItem: () => ({ when: "2025", title: "Evento" }) }),
|
|
166
|
+
f.text("after", "Frase final (depois de um clique)"),
|
|
167
|
+
f.more([f.nums("highlight", "Destacar eventos (posições, a partir de 0)"), f.bool("build", "Um por clique"), f.num("afterStep", "Frase final no clique")])],
|
|
168
|
+
table: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.tablegrid("Tabela", { hint: "A primeira linha é o cabeçalho. Cole do Excel numa célula." }),
|
|
169
|
+
f.select("style", "Estilo", [["", "Faixa (cabeçalho colorido)"], ["zebra", "Zebra"], ["linhas", "Só linhas"], ["colunas", "Uma cor por coluna"], ["cartao", "Cartão"]]),
|
|
170
|
+
f.select("color", "Cor", [["", "Do tema"], ["c1", "Cor 1 da paleta"], ["c2", "Cor 2 da paleta"], ["c3", "Cor 3 da paleta"], ["c4", "Cor 4 da paleta"], ["c5", "Cor 5 da paleta"], ["hi", "Destaque"]]),
|
|
171
|
+
f.bool("total", "Última linha é o total"), f.el("side", "Ao lado (a conclusão)", { stringAs: "text" }), f.text("caption", "Legenda"), f.text("source", "Fonte"),
|
|
172
|
+
f.more([f.num("size", "Tamanho da letra (px)"), f.bool("rowHeader", "Primeira coluna em negrito")])],
|
|
173
|
+
chart: [f.action("Atualizar da planilha", () => CTX.refreshFrom?.(), { when: (s) => s.from?.file }), f.text("kicker", "Chapéu"), f.text("title", "Título"), f.chart("chart", "Gráfico"), f.el("side", "Ao lado", { stringAs: "text" }),
|
|
174
|
+
f.more([f.num("chartHeight", "Altura do gráfico (px)"), f.num("chartStep", "Gráfico no clique"), f.num("sideStep", "Lado no clique")])],
|
|
175
|
+
compare: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.obj("left", "Lado A", COMPARE_SIDE), f.text("vs", "Entre os lados", { placeholder: "×" }),
|
|
176
|
+
f.obj("right", "Lado B", COMPARE_SIDE), f.text("after", "Frase final (depois de um clique)"),
|
|
177
|
+
f.more([f.bool("build", "Um lado por clique"), f.num("afterStep", "Frase final no clique")])],
|
|
178
|
+
matrix: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.pair("x", "Eixo horizontal", ["Esquerda", "Direita"]), f.pair("y", "Eixo vertical", ["Em cima", "Embaixo"]),
|
|
179
|
+
f.list("cells", "Quadrantes", obj(MATRIX_CELL), { addLabel: "Adicionar quadrante", max: 4, newItem: () => ({ title: "Quadrante" }) }),
|
|
180
|
+
f.more([f.bool("build", "Um por clique")])],
|
|
181
|
+
question: [f.text("kicker", "Chapéu"), f.area("question", "Pergunta"), f.text("context", "Contexto"),
|
|
182
|
+
f.list("options", "Opções", textOrObj([f.text("text", "Texto"), f.text("sub", "Detalhe"), f.text("key", "Letra")]), { addLabel: "Adicionar opção" }),
|
|
183
|
+
f.num("timer", "Timer (segundos)"), f.text("hint", "Dica"),
|
|
184
|
+
f.more([f.num("cols", "Colunas"), f.text("keys", "Letras das opções", { placeholder: "ABCD" }), f.text("timerLabel", "Rótulo do timer"), f.num("optionSize", "Tamanho das opções (px)"),
|
|
185
|
+
f.num("titleSize", "Tamanho da pergunta (px)"), f.bool("build", "Uma opção por clique")])],
|
|
186
|
+
poll: window.SagaDeveloperFields.poll(f,T),
|
|
187
|
+
image: [f.action("Editar imagem e destaques", (s) => CTX.editScreenshot?.(s)), f.text("image", "Imagem (arquivo ou link)"), f.select("fit", "Enquadramento", [["cover", "Preencher"], ["contain", "Caber inteira"]], { empty: "Preencher" }),
|
|
188
|
+
f.text("kicker", "Chapéu"), f.text("title", "Título"), f.text("caption", "Legenda"), f.more([f.el("figure", "Figura no lugar da imagem")])],
|
|
189
|
+
code: [f.text("kicker", "Chapéu"), f.text("title", "Título"), ...CODE_SOURCE_FIELDS, f.el("note", "Nota ao lado", { stringAs: "text" }),
|
|
190
|
+
f.more([...CODE_SIZE_FIELDS, f.num("noteStep", "Nota no clique")])],
|
|
191
|
+
api: [...window.SagaDeveloperFields.collection(f,obj,()=>CTX),f.text("kicker", "Chapéu"), f.text("title", "Título"), f.text("text", "Explicação curta"),
|
|
192
|
+
f.select("mode", "Modo", [["sync", "Síncrono (responde na hora)"], ["polling", "Polling (inicia e consulta até terminar)"], ["stream", "Streaming (chega aos poucos)"], ["realtime", "Tempo real (conversa por WebSocket)"]], { empty: "Síncrono (responde na hora)", structural: true }),
|
|
193
|
+
f.obj("request", "Requisição", [f.select("method", "Método", [["GET", "GET"], ["POST", "POST"], ["PUT", "PUT"], ["PATCH", "PATCH"], ["DELETE", "DELETE"]], { empty: "POST se tiver corpo" }),
|
|
194
|
+
f.text("url", "Endereço", { hint: "{{base}} e outras variáveis vêm do ambiente (Inserir › Ambientes). Segredo: {{secret.nome}}." }),
|
|
195
|
+
f.json("body", "Corpo (JSON)", { placeholder: '{ "messages": [ { "role": "user", "content": "Olá" } ] }', hint: "Também dá para editar direto no slide, na hora de apresentar." }),
|
|
196
|
+
f.bool("auth", "Enviar o token do ambiente", { default: true }),
|
|
197
|
+
f.more([f.json("headers", "Cabeçalhos (JSON)", { placeholder: '{ "X-Canal": "workshop" }' }), f.json("form", "Formulário multipart (JSON, no lugar do corpo)", { placeholder: '{ "file": "@file", "pasta": "workshop" }' })])],
|
|
198
|
+
{ when: (s) => s.mode !== "realtime" }),
|
|
199
|
+
f.json("realtime", "Conexão (WebSocket)", { when: (s) => s.mode === "realtime", placeholder: '{ "url": "{{ws}}/realtime" }', hint: "Só url é obrigatório; os outros campos seguem o formato mais comum (veja a referência)." }),
|
|
200
|
+
f.json("polling", "Polling", { when: (s) => s.mode === "polling", placeholder: '{ "id": "$.executionId", "check": { "url": "{{base}}/status/{{id}}" }, "status": "$.status", "done": ["FINISHED"] }' }),
|
|
201
|
+
f.json("stream", "Leitura do texto no streaming", { when: (s) => s.mode === "stream", placeholder: '{ "text": "$.choices[0].delta.content" }', hint: "Caminho do texto em cada pedaço SSE." }),
|
|
202
|
+
f.text("answer", "Campo da resposta em destaque", { hint: "ex.: $.choices[0].message.content", when: (s) => s.mode !== "realtime" }),
|
|
203
|
+
f.json("save", "Guardar para os próximos slides", { placeholder: '{ "path_id": "$.path_id" }', hint: "Viram {{nome}} nos slides seguintes", rows: 2 }),
|
|
204
|
+
f.text("file", "Arquivo padrão (ao lado do deck)", { hint: "para upload (@file) ou {{file.base64}}" }),
|
|
205
|
+
f.text("portal", "Link \"Abrir no portal\""),
|
|
206
|
+
f.more([f.text("token", "Este slide gera o token (caminho na resposta)", { hint: "ex.: $.access_token — os slides seguintes usam esse token" }),
|
|
207
|
+
f.text("steps", "Lista de etapas na resposta", { hint: "ex.: $.responses" }), f.text("stepText", "Texto de cada etapa", { hint: "ex.: $.output" }),
|
|
208
|
+
f.bool("mic", "Botão Gravar (microfone, para STT)"), f.text("audio", "A resposta é áudio (TTS): nome do arquivo", { placeholder: "fala.mp3" }),
|
|
209
|
+
f.select("tab", "Aba aberta ao entrar", [["body", "Corpo"], ["headers", "Cabeçalhos"], ["fields", "Parâmetros"], ["texts", "Frases (embeddings)"], ["log", "Mensagens (WebSocket)"], ["curl", "curl"], ["javascript", "JavaScript"], ["javascript-comentado", "JavaScript comentado"], ["python", "Python"], ["python-comentado", "Python comentado"]], { empty: "Corpo" }),
|
|
210
|
+
f.text("id", "Identificador (para guardar a gravação)"),
|
|
211
|
+
f.text("stepTitle", "Título de cada etapa (caminho)", { hint: "ex.: $.name — padrão tenta name, step, service e type" }),
|
|
212
|
+
f.text("tokenVar", "Nome da variável do token no código", { placeholder: "API_TOKEN" }),
|
|
213
|
+
f.json("fields", "Parâmetros documentados", { placeholder: '{ "$.campo": "O que este campo faz" }', hint: "Exibe a aba Parâmetros com caminho, valor atual e explicação." }),
|
|
214
|
+
f.json("similarity", "Comparação por embeddings", { placeholder: '{ "reference": "frase de referência", "texts": ["outra frase"], "vector": "$.data[0].embedding" }', hint: "O corpo deve usar {{text}}; compara cada frase com a referência." }),
|
|
215
|
+
f.json("code", "Abas de código", { placeholder: '["curl", "javascript", "python", "python-comentado"]', hint: "Lista de linguagens: curl, javascript, javascript-comentado, python, python-comentado." })])],
|
|
216
|
+
carousel: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.select("style", "Estilo", [["arc", "Roda em semicírculo"], ["rings", "Foto em anéis que giram"]], { empty: false, default: "arc" }),
|
|
217
|
+
f.list("items", "Itens (um por clique)", obj([f.photo("image", "Foto"), f.text("title", "Título"), f.area("text", "Texto"), f.text("label", "Chapéu do item", { placeholder: "01 / 05" })]),
|
|
218
|
+
{ addLabel: "Adicionar item", newItem: () => ({ title: "Novo item", text: "" }) })],
|
|
219
|
+
// aula (src/lessons.js): exercício resolvido, calculadora ao vivo, algoritmo animado
|
|
220
|
+
solution: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.area("problem", "Enunciado"),
|
|
221
|
+
f.list("givens", "Dados", obj([f.text("symbol", "Símbolo (LaTeX)", { mono: true }), f.text("value", "Valor", { placeholder: "0,05" }), f.text("unit", "Unidade"), f.text("label", "Nome"), f.more([f.text("latex", "Ou a linha inteira em LaTeX", { mono: true })])]), { addLabel: "Adicionar dado", newItem: () => ({ symbol: "x", value: "1", unit: "" }) }),
|
|
222
|
+
f.text("find", "Pede-se (LaTeX)", { mono: true }),
|
|
223
|
+
f.list("steps", "Passos (um por clique)", obj([f.text("text", "O que se faz"), f.area("latex", "A conta (LaTeX)", { mono: true, rows: 2 }), f.text("note", "Observação")]), { addLabel: "Adicionar passo", newItem: () => ({ text: "Próximo passo", latex: "" }) }),
|
|
224
|
+
f.obj("answer", "Resposta", [f.text("latex", "Resultado (LaTeX)", { mono: true }), f.text("text", "Em palavras"), f.text("label", "Rótulo", { placeholder: "Resposta" })]),
|
|
225
|
+
f.more([f.text("prompt", "Pergunta antes do primeiro passo", { placeholder: "Por onde você começaria?" })])],
|
|
226
|
+
calc: window.SagaCalcFields(f, obj),
|
|
227
|
+
codelab: window.SagaLabFields.codelab(f, obj),
|
|
228
|
+
graphlab: window.SagaLabFields.graphlab(f, obj),
|
|
229
|
+
algo: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.select("algorithm", "Algoritmo", [["bubble", "Bubble sort"], ["insertion", "Insertion sort"], ["selection", "Selection sort"], ["merge", "Merge sort"], ["quick", "Quicksort"], ["linear", "Busca linear"], ["binary", "Busca binária"],
|
|
230
|
+
["naive", "Busca em texto: ingênua"], ["kmp", "Busca em texto: KMP"], ["quicksearch", "Busca em texto: Quick Search"], ["bfs", "Grafo: busca em largura"], ["dijkstra", "Grafo: Dijkstra"], ["bst", "Árvore binária de busca"], ["program", "Meu código (Python)"]], { empty: false, default: "bubble", structural: true }),
|
|
231
|
+
f.nums("array", "Números (até 16)", { when: (s) => !s.program && !["naive", "kmp", "quicksearch", "bfs", "dijkstra", "program"].includes(s.algorithm) }),
|
|
232
|
+
f.num("target", "Número procurado (busca)", { when: (s) => ["linear", "binary"].includes(s.algorithm) }),
|
|
233
|
+
f.text("text", "Texto", { mono: true, when: (s) => ["naive", "kmp", "quicksearch"].includes(s.algorithm) }), f.text("pattern", "Padrão procurado", { mono: true, when: (s) => ["naive", "kmp", "quicksearch"].includes(s.algorithm) }),
|
|
234
|
+
f.json("graph", "Grafo (vizinhos de cada nó)", { hint: "{ A: [B, C], B: [D] } — com peso: { A: [[B, 4], [C, 1]] }", when: (s) => ["bfs", "dijkstra"].includes(s.algorithm) }), f.text("start", "Começa em", { when: (s) => ["bfs", "dijkstra"].includes(s.algorithm) }),
|
|
235
|
+
f.area("program", "Programa (Python simples)", { mono: true, rows: 12, hint: "Comentário no fim da linha vira a legenda do passo; {expressão} mostra o valor.", when: (s) => s.program != null || s.algorithm === "program" }),
|
|
236
|
+
f.text("predict", "Pergunta antes de rodar", { hint: "A turma aposta antes de ver; abre o slide (predict)", when: (s) => s.predict == null || typeof s.predict === "string" }),
|
|
237
|
+
f.area("explain", "Por quê (no fim)", { rows: 3, hint: "Fecha o slide depois do último passo (explain)", when: (s) => s.explain == null || typeof s.explain === "string" }),
|
|
238
|
+
f.text("call", "Chamada", { mono: true, placeholder: "meu_sort([5, 2, 9, 1])", when: (s) => s.program != null || s.algorithm === "program" }),
|
|
239
|
+
f.more([f.num("speed", "Velocidade do Tocar (ms por passo)"), f.area("code", "Código no lugar do pseudocódigo", { mono: true, rows: 6, when: (s) => !s.program && ["bubble", "insertion", "selection", "merge", "quick", "linear", "binary"].includes(s.algorithm || "bubble") }), f.text("name", "Nome mostrado"),
|
|
240
|
+
f.json("view", "O que desenhar", { hint: "[{ var: texto, pointers: [i] }, { var: padrao, under: texto, offset: i }, { var: raiz, as: tree }]", when: (s) => !["bubble", "insertion", "selection", "merge", "quick", "linear", "binary"].includes(s.algorithm || "bubble") || s.program }), f.num("maxSteps", "Máximo de passos")])],
|
|
241
|
+
// dinâmicas a dois (src/dynamics): um quadro por clique
|
|
242
|
+
duel: [f.text("kicker", "Chapéu"), f.text("title", "Título"), ...DUEL_FIELDS, f.text("bet", "Aposta antes do pull (enquete)", { placeholder: "Vai dar conflito?" })],
|
|
243
|
+
terminals: [f.text("kicker", "Chapéu"), f.text("title", "Título"),
|
|
244
|
+
f.list("steps", "Comandos (roteiro livre)", obj([WHO, f.text("cmd", "Comando", { mono: true }), f.area("out", "Saída", { mono: true, rows: 3 }), f.text("note", "Nota para a plateia")]),
|
|
245
|
+
{ addLabel: "Adicionar comando", newItem: () => ({ who: 1, cmd: "git status", out: "" }), when: (s) => s.base == null }),
|
|
246
|
+
f.pair("panes", "Nomes dos terminais", ["Pessoa A", "Pessoa B"], { when: (s) => s.base == null }),
|
|
247
|
+
f.action("Usar o roteiro do duelo (o Git calcula a saída)", (s) => { s.base = s.base ?? "linha 1\nlinha 2\nlinha 3"; s.file = s.file || "arquivo.txt"; s.turns = s.turns || [{ who: 1, edit: { 2: "mudança da A" }, commit: "muda a linha 2" }, { who: 1, push: true }, { who: 2, pull: true }, { who: 2, log: true }]; delete s.steps; CTX.commit(true); }, { when: (s) => s.base == null }),
|
|
248
|
+
...DUEL_FIELDS.map((x) => ({ ...x, when: (s) => s.base != null })), f.more([f.text("host", "Nome da máquina no prompt", { placeholder: "dev" }), f.num("keep", "Comandos visíveis por terminal")])],
|
|
249
|
+
turns: [f.text("kicker", "Chapéu"), f.text("title", "Título"),
|
|
250
|
+
f.list("people", "As duas pessoas", obj([f.text("name", "Nome"), f.text("role", "Papel", { placeholder: "revisor, cliente, servidor…" })]), { max: 2, addLabel: "Adicionar pessoa", newItem: () => ({ name: "Pessoa", role: "" }) }),
|
|
251
|
+
f.list("turns", "Falas (uma por clique)", obj([WHO, f.area("text", "Fala"), f.text("tag", "Etiqueta", { placeholder: "pergunta, aprovado, erro…" }),
|
|
252
|
+
f.more([f.text("method", "Método HTTP", { datalist: ["GET", "POST", "PUT", "PATCH", "DELETE"] }), f.text("url", "URL"), f.text("status", "Status da resposta"), f.area("code", "Código ou JSON", { mono: true, rows: 3 }), f.text("language", "Linguagem do código")])]),
|
|
253
|
+
{ addLabel: "Adicionar fala", newItem: () => ({ who: 1, text: "" }) }),
|
|
254
|
+
f.more([f.num("keep", "Falas visíveis ao mesmo tempo")])],
|
|
255
|
+
codewalk: [f.text("kicker", "Chapéu"), f.text("title", "Título"), ...CODE_SOURCE_FIELDS,
|
|
256
|
+
f.list("steps", "Etapas da explicação", obj([f.text("title", "Título da etapa"), f.area("text", "Explicação"), f.nums("highlight", "Linhas destacadas (a partir de 1)"), f.area("output", "Saída esperada (simulação)", { mono: true, rows: 3 })]),
|
|
257
|
+
{ addLabel: "Adicionar etapa", newItem: () => ({ title: "Próximo passo", text: "", highlight: [1] }) }),
|
|
258
|
+
f.more(CODE_SIZE_FIELDS)],
|
|
259
|
+
spotlight: [f.action("Editar imagem e destaques", (s) => CTX.editScreenshot?.(s)), f.text("kicker", "Chapéu"), f.text("title", "Título"),
|
|
260
|
+
f.text("caption", "Legenda"), f.bool("zoom", "Aproximar devagar de cada detalhe (zoom)"), f.more([f.text("image", "Imagem (arquivo ou link)"), f.list("hotspots", "Detalhes em foco", obj([f.select("kind", "Tipo", [["area", "Área"], ["point", "Ponto"]]), f.text("title", "Título do detalhe"), f.area("text", "Explicação"),
|
|
261
|
+
f.num("x", "Posição horizontal (%)", { min: 0, max: 96 }), f.num("y", "Posição vertical (%)", { min: 0, max: 96 }),
|
|
262
|
+
f.num("width", "Largura (%)", { min: 4, max: 100 }), f.num("height", "Altura (%)", { min: 4, max: 100 }), f.num("zoom", "Zoom neste detalhe (1 = imagem inteira)", { min: 1, max: 4 })]),
|
|
263
|
+
{ addLabel: "Adicionar detalhe", newItem: () => ({ title: "Novo detalhe", text: "", x: 10, y: 10, width: 30, height: 25 }) })]),
|
|
264
|
+
f.more([f.el("figure", "Figura no lugar da imagem")])],
|
|
265
|
+
decisionlab: [f.text("title", "Título"), f.obj("lab", "Premissas iniciais", [f.num("volume", "Número de decisões"), f.num("errorRate", "Erros da automação (%)"), f.num("reviewRate", "Casos revisados (%)"), f.num("catchRate", "Erros corrigidos (%)"), f.num("introducedRate", "Acertos estragados (%)"), f.num("seconds", "Segundos por revisão")])],
|
|
266
|
+
infographic: [f.select("shape", "Forma", [["arco", "Arco (pílulas numeradas)"], ["ramos", "Ramos (cartões)"], ["lados", "Lados (ícones dos dois lados)"], ["trilhas", "Trilhas (objetivo e etapas)"], ["metro", "Metrô (linhas até cada item)"], ["ciclo", "Ciclo (etapas em volta, com setas)"]]),
|
|
267
|
+
f.text("kicker", "Texto menor"), f.text("title", "Título"),
|
|
268
|
+
f.obj("center", "Centro", [f.text("title", "Texto"), f.text("text", "Detalhe"), f.icon("icon", "Ícone")]),
|
|
269
|
+
f.list("items", "Itens (2 a 8)", obj([f.text("title", "Título"), f.area("text", "Texto"), f.icon("icon", "Ícone"),
|
|
270
|
+
f.list("steps", "Etapas seguintes (só na forma Trilhas)", obj([f.text("title", "Título"), f.area("text", "Texto"), f.icon("icon", "Ícone")]), { addLabel: "Adicionar etapa", newItem: () => ({ title: "Etapa" }), max: 3 })]),
|
|
271
|
+
{ addLabel: "Adicionar item", newItem: () => ({ title: "Novo item", text: "" }), max: 8 }),
|
|
272
|
+
f.text("caption", "Legenda")],
|
|
273
|
+
diagram: [f.text("kicker", "Texto menor"), f.text("title", "Título"), f.area("mermaid", "Diagrama (Mermaid: flowchart, sequenceDiagram, stateDiagram-v2, classDiagram, mindmap…)", { mono: true, rows: 12 }), f.select("curve", "Ligações", [["curva", "Curvas"], ["angulo", "Em ângulo"], ["reta", "Retas"]], { empty: "Curvas" }), f.text("caption", "Legenda")],
|
|
274
|
+
science: [f.action("Atualizar da planilha", () => CTX.refreshFrom?.(), { when: (s) => s.from?.file }), f.text("title", "Título"), f.list("equations", "Equações (até 5)", obj([f.text("label", "Legenda"), f.area("latex", "Equação em LaTeX", {mono:true, rows:2})]), {addLabel:"Adicionar equação", newItem:()=>({latex:"E = mc^2"}), max:5}), f.action("Adicionar gráfico", s=>{s.plot={functions:["a*sin(b*x)"]};CTX.commit(true);}, {when:s=>s.plot===false}), f.action("Remover gráfico", s=>{s.plot=false;CTX.commit(true);}, {when:s=>s.plot!==false}), f.plot("plot", "Gráfico", {when:s=>s.plot!==false}), f.text("caption", "Nota")],
|
|
275
|
+
scenography: [f.select("scene", "Composição", [["stage","Palco e profundidade"],["floor","Chão em perspectiva"],["signs","Placas na cidade"],["terminal","Terminal hacker"],["cafe","Lousa de café"],["travel","Cartão de embarque"],["ticker","Pregão financeiro"],["marquee","Letreiro de cinema"],["blueprint","Planta técnica"],["magazine","Capa de revista"],["orbit","Órbita"],["synthwave","Neon anos 80"],["gallery","Parede de galeria"]]), f.area("title", "Texto no cenário", {rows:2}), f.text("kicker", "Texto menor"), f.text("subtitle", "Subtítulo"), f.text("caption", "Assinatura"), f.text("image", "Imagem de fundo (arquivo ou link)"), f.num("imageOpacity", "Transparência do fundo (0 a 1; padrão 0.85)", { placeholder: "0.85" }), f.text("foreground", "Recorte em primeiro plano (PNG transparente)"), f.num("titleSize", "Tamanho do texto (px)")],
|
|
276
|
+
kinetic: [f.el("figure", "Cena de fundo"), f.list("beats", "Frases da sequência", obj([
|
|
277
|
+
f.area("text", "Frase", { rows: 2 }),
|
|
278
|
+
f.select("style", "Estilo tipográfico", [["clean", "Limpo"], ["poster", "Pôster"], ["editorial", "Editorial"], ["outline", "Contorno"], ["marker", "Marca-texto"]], { empty: "Limpo" }),
|
|
279
|
+
f.select("position", "Posição", [["left", "Esquerda"], ["center", "Centro"], ["right", "Direita"], ["top", "Alto"], ["bottom", "Baixo"]], { empty: "Variar automaticamente" }),
|
|
280
|
+
f.select("color", "Cor", [["white", "Branco"], ["gold", "Dourado"], ["pink", "Rosa"], ["cyan", "Ciano"]], { empty: "Branco" }),
|
|
281
|
+
f.select("size", "Escala", [["small", "Pequena"], ["medium", "Média"], ["large", "Grande"]], { empty: "Grande" }),
|
|
282
|
+
f.text("tag", "Microlegenda (opcional)"),
|
|
283
|
+
]), { addLabel: "Adicionar frase", newItem: () => ({ text: "Nova frase" }) }),
|
|
284
|
+
f.more([f.num("interval", "Tempo por frase (ms)", { min: 450, max: 5000, placeholder: "1000" }), f.bool("autoplay", "Reproduzir automaticamente", { default: true })])],
|
|
285
|
+
blocks: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.els("content", "Blocos"), f.more([f.select("titleAs", "Estilo do título", TEXT_ROLES, { empty: "Título" })])],
|
|
286
|
+
end: [f.text("kicker", "Chapéu"), f.area("title", "Título", { placeholder: "Obrigado." }), f.text("subtitle", "Subtítulo"), f.list("contacts", "Contatos", T, { addLabel: "Adicionar contato" }),
|
|
287
|
+
f.text("qr", "QR code (link)", { hint: "Ex.: seu LinkedIn. Aparece ao lado, pronto para a câmera do celular." }), f.text("qrLabel", "Legenda do QR code"),
|
|
288
|
+
f.el("figure", "Figura"), f.more([f.num("titleSize", "Tamanho do título (px)")])],
|
|
289
|
+
references: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("items", "Referências", TA, { addLabel: "Adicionar referência" })],
|
|
290
|
+
video: window.SagaDeveloperFields.video(f,()=>CTX),
|
|
291
|
+
playground: window.SagaDeveloperFields.playground(f),
|
|
292
|
+
portal: window.SagaDeveloperFields.portal(f,obj),
|
|
293
|
+
canvas: [f.els("elements", "Elementos", { positioned: true })],
|
|
294
|
+
headline: [f.text("kicker", "Chapéu"), f.area("text", "Frase"), f.text("caption", "Legenda"),
|
|
295
|
+
f.more([f.select("as", "Estilo", TEXT_ROLES, { empty: "Enorme" }), f.num("size", "Tamanho máximo (px)")])],
|
|
296
|
+
full: [f.area("image_prompt", "Imagem gerada pela IA (descreva a página)", { hint: "A IA gera a imagem que ocupa o slide inteiro." }),
|
|
297
|
+
f.action("Gerar imagem agora", (s, btn) => CTX.generateImage?.(s, btn), { when: (s) => !s.image }),
|
|
298
|
+
f.text("image", "…ou arquivo/link da imagem"), f.el("figure", "…ou uma figura (ícone, gráfico, diagrama)"), f.text("kicker", "Chapéu"), f.area("title", "Título por cima"), f.text("caption", "Legenda"),
|
|
299
|
+
f.select("overlay", "Posição do texto", [["bottom", "Embaixo"], ["left", "À esquerda"], ["center", "No centro"], ["none", "Sem texto"]], { empty: "Automático" }),
|
|
300
|
+
f.more([f.select("fit", "Enquadramento", [["cover", "Preencher"], ["contain", "Caber inteira"]]), f.num("titleSize", "Tamanho do título (px)")])],
|
|
301
|
+
funnel: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("stages", "Etapas do funil", obj([f.text("title", "Etapa"), f.text("value", "Valor"), f.text("text", "Comentário"), f.bool("hl", "Destacar")]),
|
|
302
|
+
{ addLabel: "Adicionar etapa", newItem: () => ({ title: "Etapa", value: "" }) }), f.more([f.bool("build", "Uma por clique")])],
|
|
303
|
+
pyramid: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.list("levels", "Níveis (do topo para a base)", obj([f.text("title", "Nível"), f.text("text", "Comentário"), f.bool("hl", "Destacar")]),
|
|
304
|
+
{ addLabel: "Adicionar nível", newItem: () => ({ title: "Nível" }) }), f.more([f.bool("build", "Um por clique")])],
|
|
305
|
+
agenda: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.num("current", "Seção atual (número)"),
|
|
306
|
+
f.list("items", "Seções", obj([f.text("title", "Seção"), f.text("text", "Detalhe"), f.text("time", "Tempo")]), { addLabel: "Adicionar seção", newItem: () => ({ title: "Seção" }) }),
|
|
307
|
+
f.more([f.bool("build", "Uma por clique")])],
|
|
308
|
+
bento: [f.text("kicker", "Chapéu"), f.text("title", "Título"),
|
|
309
|
+
f.list("tiles", "Blocos", obj([f.text("title", "Título"), f.text("value", "Número grande"), f.area("text", "Texto"), f.icon("icon", "Ícone"),
|
|
310
|
+
f.select("size", "Tamanho", [["big", "Grande (2×2)"], ["wide", "Largo (2×1)"], ["tall", "Alto (1×2)"]], { empty: "Normal" }), f.bool("hl", "Destacar"),
|
|
311
|
+
f.more([f.el("figure", "Figura (no lugar do ícone)")])]), { addLabel: "Adicionar bloco", newItem: () => ({ title: "Bloco", icon: "star" }) }),
|
|
312
|
+
f.more([f.num("cols", "Colunas"), f.bool("build", "Um por clique")])],
|
|
313
|
+
};
|
|
314
|
+
const SLIDE_RESERVED = new Set(["layout", "tone", "deco", "notes", "consulta", "time", "auto", "density", "visualEdits", "from", "fiscalOk", "uid", "review", "original"]); // uid: identidade do slide; review/original: revisão e origem (importação) // from: de que planilha veio o gráfico (Atualizar da planilha)
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------------------------
|
|
317
|
+
// Elementos (src/elements.js): tipos, como reconhecer e campos
|
|
318
|
+
// ---------------------------------------------------------------------------------------------
|
|
319
|
+
const EL_MORE = (positioned) => [
|
|
320
|
+
f.num("step", "Aparece no clique"), f.num("exit", "Some no clique"), f.select("anim", "Animação", ANIMS),
|
|
321
|
+
f.text("continuity", "Mesmo objeto entre slides", { hint: "Repita este nome no objeto da próxima cena para acompanhar a mudança de posição e tamanho." }),
|
|
322
|
+
...(positioned ? [f.num("x", "X (px)"), f.num("y", "Y (px)")] : []),
|
|
323
|
+
f.num("w", "Largura (px)"), f.num("h", "Altura (px)"), f.color("color", "Cor"), f.color("bg", "Fundo"),
|
|
324
|
+
f.select("align", "Alinhamento", [["left", "Esquerda"], ["center", "Centro"], ["right", "Direita"]]),
|
|
325
|
+
f.select("card", "Moldura", [["true", "Cartão"], ["hi", "Cartão destacado"]], { parse: (v) => (v === "true" ? true : v) }),
|
|
326
|
+
f.num("flex", "Peso (flex)"), f.num("pad", "Espaço interno (px)"),
|
|
327
|
+
];
|
|
328
|
+
const ELEMENT_KINDS = [
|
|
329
|
+
{ id: "icon", label: "Ícone", is: (e) => e.icon, tpl: () => ({ icon: "star", size: 200 }),
|
|
330
|
+
fields: [f.icon("icon", "Ícone"), f.num("size", "Tamanho (px)"), f.num("stroke", "Espessura do traço")] },
|
|
331
|
+
{ id: "picto", label: "Pessoa / cena", is: (e) => e.picto, tpl: () => ({ picto: "human", pose: "stand" }),
|
|
332
|
+
fields: [f.select("picto", "Tipo", [["human", "Pessoa"], ["crowd", "Multidão"], ["scene", "Cena"], ["machine", "Máquina"]]),
|
|
333
|
+
f.select("pose", "Pose", POSES, { when: (e) => e.picto === "human" || e.picto === "crowd" }),
|
|
334
|
+
f.select("sign", "Placa atrás", [["circle", "Círculo"], ["square", "Quadrado"], ["triangle", "Triângulo"]], { when: (e) => e.picto === "human" }),
|
|
335
|
+
f.select("name", "Cena", [["console", "Mesa de controle"], ["desk", "Mesa de trabalho"], ["pair", "Dupla na mesa"], ["judge", "Juiz"], ["elevator", "Elevador"], ["car-top", "Carro visto de cima"]], { when: (e) => e.picto === "scene" }),
|
|
336
|
+
f.text("screen", "Texto na tela", { when: (e) => e.name === "console" }), f.num("screenSize", "Tamanho do texto da tela", { when: (e) => e.name === "console" }),
|
|
337
|
+
f.bool("alarm", "Alarme", { when: (e) => e.name === "console" }),
|
|
338
|
+
f.select("driver", "Motorista", CAR_SEAT, { when: (e) => e.name === "car-top" }), f.select("passenger", "Passageiro", CAR_SEAT, { when: (e) => e.name === "car-top" }),
|
|
339
|
+
f.select("back", "Banco de trás", [["sleep", "Pessoa dormindo"], ["human", "Pessoa"], ["none", "Vazio"]], { when: (e) => e.name === "car-top" }),
|
|
340
|
+
f.bool("button", "Botão de parada", { when: (e) => e.name === "car-top" || e.name === "elevator" }),
|
|
341
|
+
f.num("count", "Quantidade", { when: (e) => e.picto === "crowd" }), f.num("highlight", "Destacados", { when: (e) => e.picto === "crowd" })] },
|
|
342
|
+
{ id: "image", label: "Imagem", is: (e) => e.image || e.image_prompt, tpl: () => ({ image: "", fit: "cover" }),
|
|
343
|
+
fields: [f.action("Escolher minha foto", (e) => CTX.chooseImage?.(e)),
|
|
344
|
+
f.action("Criar imagem pelo conteúdo do slide", (e) => CTX.imageChat?.(e)),
|
|
345
|
+
f.text("image", "Arquivo ou link"), f.area("image_prompt", "Descrição para a IA gerar", { hint: "Descreva a nova imagem. A geração substitui a foto e preserva sua posição." }),
|
|
346
|
+
f.action("Gerar imagem agora", (e, btn) => CTX.generateImage?.(e, btn)),
|
|
347
|
+
f.select("fit", "Enquadramento", [["cover", "Preencher"], ["contain", "Caber inteira"]]), f.text("alt", "Texto alternativo"), f.num("radius", "Cantos (px)")] },
|
|
348
|
+
{ id: "chart", label: "Gráfico", is: (e) => e.chart, tpl: () => ({ chart: "bar", data: [{ label: "A", value: 10 }, { label: "B", value: 20 }] }), chart: true },
|
|
349
|
+
{ id: "ufmap", label: "Mapa por UF", is: (e) => e.ufmap, tpl: () => ({ ufmap: { SP: 30, RJ: 15, MG: 18 } }),
|
|
350
|
+
fields: [f.json("ufmap", "Valores por UF", { placeholder: '{ "SP": 120, "RJ": 80 }', hint: "Sigla da UF e o valor; a cor mais forte é o maior." }),
|
|
351
|
+
f.text("prefix", "Antes do valor"), f.text("suffix", "Depois do valor"), f.list("highlight", "Destacar UFs", T, { addLabel: "Adicionar UF" }), f.text("legend", "Legenda"),
|
|
352
|
+
f.bool("showValues", "Mostrar os valores")] },
|
|
353
|
+
{ id: "diagram", label: "Diagrama", is: (e) => e.diagram, tpl: () => ({ diagram: "flow", steps: ["Início", "Meio", "Fim"] }),
|
|
354
|
+
fields: [f.select("diagram", "Tipo", [["flow", "Fluxo"], ["loop", "Ciclo"], ["spectrum", "Espectro"], ["venn", "Venn"]]),
|
|
355
|
+
f.list("steps", "Etapas", T, { when: (e) => e.diagram === "flow" }), f.num("highlight", "Etapa destacada", { when: (e) => e.diagram === "flow" }),
|
|
356
|
+
f.list("nodes", "Pontos do ciclo", T, { when: (e) => e.diagram === "loop" }), f.select("actor", "Pessoa no ciclo", [["in", "Dentro"], ["on", "No ciclo"], ["out", "Fora"]], { when: (e) => e.diagram === "loop" }),
|
|
357
|
+
f.list("stops", "Pontos do espectro", T, { when: (e) => e.diagram === "spectrum" }), f.text("left", "Extremo esquerdo", { when: (e) => e.diagram === "spectrum" }), f.text("right", "Extremo direito", { when: (e) => e.diagram === "spectrum" }),
|
|
358
|
+
f.num("at", "Posição marcada", { when: (e) => e.diagram === "loop" || e.diagram === "spectrum" }),
|
|
359
|
+
f.text("a", "Conjunto A", { when: (e) => e.diagram === "venn" }), f.text("b", "Conjunto B", { when: (e) => e.diagram === "venn" }), f.text("both", "Interseção", { when: (e) => e.diagram === "venn" })] },
|
|
360
|
+
{ id: "qr", label: "QR code", is: (e) => e.qr, tpl: () => ({ qr: "https://", size: 360, label: "Aponte a câmera" }),
|
|
361
|
+
fields: [f.text("qr", "Link ou texto", { hint: "Ex.: seu LinkedIn — https://linkedin.com/in/voce" }), f.text("label", "Legenda"), f.num("size", "Tamanho (px)")] },
|
|
362
|
+
{ id: "svg", label: "SVG", is: (e) => e.svg, tpl: () => ({ svg: "<svg viewBox='0 0 100 100'></svg>" }), fields: [f.area("svg", "SVG", { mono: true, rows: 6 })] },
|
|
363
|
+
{ id: "text", label: "Texto", is: (e) => e.text != null || textRole(e), tpl: () => ({ body: "Texto" }), text: true },
|
|
364
|
+
{ id: "row", label: "Linha (lado a lado)", is: (e) => e.row, tpl: () => ({ row: [{ body: "Esquerda" }, { body: "Direita" }] }),
|
|
365
|
+
fields: [f.els("row", "Itens da linha"), f.num("gap", "Espaço entre itens (px)"),
|
|
366
|
+
f.select("valign", "Alinhamento vertical", [["stretch", "Esticar"], ["flex-start", "Topo"], ["center", "Centro"], ["flex-end", "Base"]]),
|
|
367
|
+
f.select("justify", "Distribuição", [["flex-start", "Início"], ["center", "Centro"], ["space-between", "Espalhar"], ["flex-end", "Fim"]])] },
|
|
368
|
+
{ id: "col", label: "Coluna (empilhado)", is: (e) => e.col, tpl: () => ({ col: [{ body: "Primeiro" }, { body: "Segundo" }] }),
|
|
369
|
+
fields: [f.els("col", "Itens da coluna"), f.num("gap", "Espaço entre itens (px)"),
|
|
370
|
+
f.select("justify", "Distribuição", [["flex-start", "Início"], ["center", "Centro"], ["space-between", "Espalhar"], ["flex-end", "Fim"]]),
|
|
371
|
+
f.select("items", "Alinhamento horizontal", [["stretch", "Esticar"], ["flex-start", "Esquerda"], ["center", "Centro"], ["flex-end", "Direita"]])] },
|
|
372
|
+
{ id: "counter", label: "Contador animado", is: (e) => e.counter != null, tpl: () => ({ counter: 100, from: 0 }),
|
|
373
|
+
fields: [f.num("counter", "Valor"), f.num("from", "Contar a partir de"), f.text("prefix", "Antes"), f.text("suffix", "Depois"), f.num("decimals", "Casas decimais"), f.num("size", "Tamanho (px)"), f.text("label", "Rótulo")] },
|
|
374
|
+
{ id: "timer", label: "Timer", is: (e) => e.timer != null, tpl: () => ({ timer: 30 }),
|
|
375
|
+
fields: [f.num("timer", "Segundos"), f.text("label", "Rótulo"), f.bool("auto", "Começa sozinho", { default: true }), f.num("size", "Tamanho (px)")] },
|
|
376
|
+
{ id: "poll", label: "Enquete", is: (e) => e.poll, tpl: () => ({ poll: "enquete-1", options: ["Sim", "Não"] }),
|
|
377
|
+
fields: [f.text("poll", "Identificador"), f.list("options", "Opções", T), f.text("compare", "Comparar com (identificador)"), f.text("hint", "Dica")] },
|
|
378
|
+
{ id: "list", label: "Lista", is: (e) => e.list, tpl: () => ({ list: ["Primeiro", "Segundo"] }),
|
|
379
|
+
fields: [f.list("list", "Itens", textOrObj([f.area("text", "Texto"), f.text("sub", "Detalhe")])), f.bool("numbered", "Numerada"), f.bool("build", "Um por clique"), f.num("size", "Tamanho (px)")] },
|
|
380
|
+
{ id: "cards", label: "Cartões", is: (e) => e.cards, tpl: () => ({ cards: [{ icon: "star", title: "Cartão" }] }),
|
|
381
|
+
fields: [f.list("cards", "Cartões", obj(CARD), { newItem: () => ({ icon: "star", title: "Cartão" }) }), f.num("cols", "Colunas"), f.bool("build", "Um por clique")] },
|
|
382
|
+
{ id: "stats", label: "Indicadores", is: (e) => e.stats || e.kpis, tpl: () => ({ stats: [{ value: "100%", label: "Indicador" }] }),
|
|
383
|
+
fields: [f.list(["stats", "kpis"], "Indicadores", obj(STAT), { newItem: () => ({ value: "100%", label: "Indicador" }) }), f.num("cols", "Colunas"), f.bool("build", "Um por clique")] },
|
|
384
|
+
{ id: "steps", label: "Etapas", is: (e) => e.steps || e.process || e.flow, tpl: () => ({ steps: [{ title: "Etapa 1" }, { title: "Etapa 2" }] }),
|
|
385
|
+
fields: [f.list(["steps", "process", "flow"], "Etapas", obj(STEP), { newItem: () => ({ title: "Etapa" }) }), f.num("cols", "Colunas"), f.bool("build", "Uma por clique")] },
|
|
386
|
+
{ id: "progress", label: "Barra de progresso", is: (e) => e.progress != null, tpl: () => ({ progress: 60, label: "Progresso" }),
|
|
387
|
+
fields: [f.num("progress", "Valor (%)"), f.text("label", "Rótulo")] },
|
|
388
|
+
{ id: "tags", label: "Etiquetas", is: (e) => e.tags || e.chips, tpl: () => ({ tags: ["Etiqueta"] }), fields: [f.list(["tags", "chips"], "Etiquetas", T)] },
|
|
389
|
+
{ id: "rating", label: "Avaliação", is: (e) => e.rating != null, tpl: () => ({ rating: 4 }), fields: [f.num("rating", "Nota"), f.text("label", "Rótulo")] },
|
|
390
|
+
{ id: "code", label: "Código", is: (e) => e.code, tpl: () => ({ code: "print('olá')" }), fields: [f.area("code", "Código", { mono: true, rows: 6 }), f.nums("highlight", "Linhas destacadas"), f.num("size", "Tamanho (px)")] },
|
|
391
|
+
{ id: "shape", label: "Forma", is: (e) => e.shape, tpl: () => ({ shape: "rounded", w: 300, h: 160, fill: "hi" }),
|
|
392
|
+
fields: [f.select("shape", "Forma", [["rect", "Retângulo"], ["rounded", "Arredondado"], ["circle", "Círculo"], ["pill", "Pílula"], ["line", "Linha"]]), f.color("fill", "Preenchimento"), f.color("stroke", "Contorno")] },
|
|
393
|
+
{ id: "badge", label: "Selo", is: (e) => e.badge, tpl: () => ({ badge: "NOVO" }), fields: [f.text("badge", "Texto")] },
|
|
394
|
+
{ id: "aviso", label: "Aviso", is: (e) => e.aviso,
|
|
395
|
+
tpl: () => ({ aviso: { tipo: "dica", titulo: "Dica", texto: "Texto do aviso" } }),
|
|
396
|
+
fields: [f.obj("aviso", "Aviso", [f.select("tipo", "Tipo", [["importante", "Importante"], ["atencao", "Atenção"], ["dica", "Dica"], ["perigo", "Perigo"]]), f.text("titulo", "Título"), f.area("texto", "Texto")])] },
|
|
397
|
+
{ id: "video", label: "Vídeo", is: (e) => e.video, tpl: () => ({ video: "https://", label: "Assistir" }), fields: [f.text("video", "Link"), f.text("label", "Texto do botão")] },
|
|
398
|
+
{ id: "widget", label: "Widget", is: (e) => e.widget, tpl: () => ({ widget: "" }), fields: [f.text("widget", "Nome do widget")] },
|
|
399
|
+
{ id: "html", label: "HTML", is: (e) => e.html, tpl: () => ({ html: "<div></div>" }), fields: [f.area("html", "HTML", { mono: true, rows: 6 })] },
|
|
400
|
+
{ id: "spacer", label: "Espaço", is: (e) => e.spacer != null, tpl: () => ({ spacer: 40 }), fields: [f.num("spacer", "Tamanho (px) — vazio = empurrar")] },
|
|
401
|
+
];
|
|
402
|
+
const TEXT_KEYS = TEXT_ROLES.map(([r]) => r);
|
|
403
|
+
function textRole(e) {
|
|
404
|
+
return TEXT_KEYS.find((r) => e[r] != null && typeof e[r] !== "object") || null;
|
|
405
|
+
}
|
|
406
|
+
function kindOf(e) {
|
|
407
|
+
if (e == null) return null;
|
|
408
|
+
if (typeof e !== "object") return ELEMENT_KINDS.find((k) => k.id === "text");
|
|
409
|
+
// mesma ordem de el() em src/elements.js: figuras, texto, estruturas, widgets, atalhos de texto
|
|
410
|
+
const order = ["icon", "picto", "diagram", "chart", "svg", "image", "text", "row", "col", "counter", "timer", "poll", "list", "cards", "stats", "steps",
|
|
411
|
+
"progress", "tags", "rating", "code", "shape", "badge", "aviso", "video", "widget", "html", "spacer"];
|
|
412
|
+
for (const id of order) {
|
|
413
|
+
const k = ELEMENT_KINDS.find((x) => x.id === id);
|
|
414
|
+
if (id === "text" ? e.text != null : k.is(e)) return k;
|
|
415
|
+
}
|
|
416
|
+
return textRole(e) ? ELEMENT_KINDS.find((k) => k.id === "text") : null;
|
|
417
|
+
}
|
|
418
|
+
// chaves que cada tipo usa (o resto do elemento aparece em "Outros campos")
|
|
419
|
+
const EL_COMMON_KEYS = ["step", "exit", "anim", "x", "y", "w", "h", "color", "bg", "align", "card", "flex", "pad", "class", "style", "id", "opacity", "rotate"];
|
|
420
|
+
|
|
421
|
+
// Gráficos (src/figures/charts.js)
|
|
422
|
+
const CHART_TYPES = [["bar", "Barras"], ["column", "Colunas"], ["line", "Linhas"], ["donut", "Rosca"], ["waffle", "Waffle"], ["isotype", "Ícones"], ["stacked", "Empilhado"]];
|
|
423
|
+
const DATA_ITEM = obj([f.text("label", "Rótulo"), f.num("value", "Valor"), f.color("color", "Cor")]);
|
|
424
|
+
const CHART_FIELDS = {
|
|
425
|
+
bar: [f.sheet("Dados"), f.text("suffix", "Unidade"), f.num("max", "Máximo"), f.nums("highlight", "Destacar (posições)")],
|
|
426
|
+
column: [f.sheet("Dados"), f.text("suffix", "Unidade"), f.num("max", "Máximo"), f.nums("highlight", "Destacar (posições)")],
|
|
427
|
+
stacked: [f.sheet("Dados", { single: true }), f.text("suffix", "Unidade")],
|
|
428
|
+
line: [f.sheet("Dados", { multi: true }),
|
|
429
|
+
f.num("min", "Mínimo"), f.num("max", "Máximo"), f.nums("ticks", "Marcas do eixo (valores)"), f.text("suffix", "Unidade"), f.bool("area", "Preencher área"), f.bool("markers", "Marcadores", { default: true }), f.bool("axis", "Eixo", { default: true }),
|
|
430
|
+
f.list("bands", "Faixas", obj([f.num("at", "Posição"), f.text("text", "Texto")]), { newItem: () => ({ at: 0, text: "Faixa" }) }),
|
|
431
|
+
f.list("annotations", "Anotações", obj([f.num("at", "Posição"), f.text("text", "Texto")]), { newItem: () => ({ at: 0, text: "Nota" }) })],
|
|
432
|
+
donut: [f.num("value", "Valor (%)"), f.text("center", "Texto no centro"), f.sheet("Partes (no lugar do valor)", { single: true, key: "parts" })],
|
|
433
|
+
waffle: [f.num("total", "Total de quadrados"), f.num("cols", "Colunas"), f.list("groups", "Grupos", obj([f.num("count", "Quantidade"), f.text("label", "Rótulo"), f.color("color", "Cor")]), { newItem: () => ({ count: 10, label: "Grupo" }) })],
|
|
434
|
+
isotype: [f.num("total", "Total"), f.num("highlight", "Destacados"), f.icon("icon", "Ícone")],
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------------------------
|
|
438
|
+
// DOM
|
|
439
|
+
// ---------------------------------------------------------------------------------------------
|
|
440
|
+
function h(tag, props = {}, ...kids) {
|
|
441
|
+
const el = document.createElement(tag);
|
|
442
|
+
for (const [k, v] of Object.entries(props || {})) {
|
|
443
|
+
if (v == null || v === false) continue;
|
|
444
|
+
if (k === "class") el.className = v;
|
|
445
|
+
else if (k === "text") el.textContent = v;
|
|
446
|
+
else if (k.startsWith("on")) el.addEventListener(k.slice(2), v);
|
|
447
|
+
else el.setAttribute(k, v === true ? "" : v);
|
|
448
|
+
}
|
|
449
|
+
for (const kid of kids.flat()) if (kid != null && kid !== false) el.append(kid.nodeType ? kid : document.createTextNode(String(kid)));
|
|
450
|
+
return el;
|
|
451
|
+
}
|
|
452
|
+
const icon = (name) => h("i", { class: "ic", "data-ic": name });
|
|
453
|
+
|
|
454
|
+
const isEmpty = (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
455
|
+
function setKey(o, k, v) {
|
|
456
|
+
if (isEmpty(v)) delete o[k];
|
|
457
|
+
else o[k] = v;
|
|
458
|
+
}
|
|
459
|
+
// campo de lista que aceita nomes alternativos (stats|kpis|items): usa o que existir
|
|
460
|
+
const listKey = (o, k) => (Array.isArray(k) ? k.find((x) => Array.isArray(o[x])) || k[0] : k);
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------------------------
|
|
463
|
+
// Renderização
|
|
464
|
+
// ---------------------------------------------------------------------------------------------
|
|
465
|
+
let CTX = null;
|
|
466
|
+
let typingTimer = null;
|
|
467
|
+
const commitSoon = () => { clearTimeout(typingTimer); typingTimer = setTimeout(() => CTX.commit(false), 350); };
|
|
468
|
+
// microtask: roda depois de todos os handlers do mesmo evento (ex.: o que devolve "texto ou objeto" ao array)
|
|
469
|
+
const commitNow = () => { clearTimeout(typingTimer); queueMicrotask(() => CTX.commit(false)); };
|
|
470
|
+
const commitStructure = () => { clearTimeout(typingTimer); queueMicrotask(() => CTX.commit(true)); };
|
|
471
|
+
|
|
472
|
+
// Item recolhível com cabeçalho próprio (botões no cabeçalho não abrem/fecham o item)
|
|
473
|
+
function collapsible(path, title, controls, body, extraClass = "") {
|
|
474
|
+
const wrap = h("div", { class: `sf-item ${extraClass}` });
|
|
475
|
+
const toggle = h("button", { class: "sf-item-toggle", type: "button", "aria-expanded": "false" }, icon("chevron-down"), h("span", { class: "sf-item-title", text: title }));
|
|
476
|
+
const set = (open) => {
|
|
477
|
+
wrap.classList.toggle("open", open);
|
|
478
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
479
|
+
open ? openGroups.add(path) : openGroups.delete(path);
|
|
480
|
+
};
|
|
481
|
+
toggle.onclick = () => set(!wrap.classList.contains("open"));
|
|
482
|
+
wrap.append(h("div", { class: "sf-item-head" }, toggle, ...controls), h("div", { class: "sf-item-body" }, body));
|
|
483
|
+
set(openGroups.has(path));
|
|
484
|
+
return wrap;
|
|
485
|
+
}
|
|
486
|
+
const openGroups = new Set(); // grupos "Mais opções"/itens abertos, por caminho — sobrevive a reconstruções
|
|
487
|
+
|
|
488
|
+
function fieldWrap(spec, control, extraClass = "") {
|
|
489
|
+
return h("div", { class: `sf-field ${extraClass}` },
|
|
490
|
+
h("label", { class: "sf-label", text: spec.label }), control,
|
|
491
|
+
spec.hint ? h("div", { class: "sf-hint", text: spec.hint }) : null);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function renderField(o, spec, path, controls) {
|
|
495
|
+
if (spec.when && !spec.when(o)) return null;
|
|
496
|
+
switch (spec.type) {
|
|
497
|
+
case "text": case "textarea": {
|
|
498
|
+
const node = textField(o, spec);
|
|
499
|
+
if (spec.k) controls?.set(spec.k, node);
|
|
500
|
+
return node;
|
|
501
|
+
}
|
|
502
|
+
case "number": return numberField(o, spec);
|
|
503
|
+
case "bool": return boolField(o, spec);
|
|
504
|
+
case "select": return selectField(o, spec);
|
|
505
|
+
case "codeLanguage": return codeLanguageField(o, spec, controls?.get(spec.filenameKey));
|
|
506
|
+
case "color": return textField(o, { ...spec, datalist: COLOR_ROLES, placeholder: spec.placeholder || "fg, hi, em… ou #hex" });
|
|
507
|
+
case "icon": return iconField(o, spec);
|
|
508
|
+
case "nums": return numsField(o, spec);
|
|
509
|
+
case "pair": return pairField(o, spec);
|
|
510
|
+
case "list": return listField(o, spec, path);
|
|
511
|
+
case "object": return objectField(o, spec, path);
|
|
512
|
+
case "element": return elementField(o, spec, path);
|
|
513
|
+
case "elements": return elementsField(o, spec, path);
|
|
514
|
+
case "chart": return chartField(o, spec, path);
|
|
515
|
+
case "plot": return plotField(o, spec);
|
|
516
|
+
case "sheet": return sheetField(o, spec);
|
|
517
|
+
case "tablegrid": return tableGridField(o, spec);
|
|
518
|
+
case "linemap": return lineMapField(o, spec);
|
|
519
|
+
case "photo": return photoField(o, spec);
|
|
520
|
+
case "json": return jsonField(o, spec);
|
|
521
|
+
case "more": return moreGroup(o, spec.fields, path + ".more");
|
|
522
|
+
case "action": {
|
|
523
|
+
const b = h("button", { class: "btn btn-secondary sf-action", type: "button" }, spec.label);
|
|
524
|
+
b.onclick = () => spec.run(o, b);
|
|
525
|
+
return b;
|
|
526
|
+
}
|
|
527
|
+
default: return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function textField(o, spec) {
|
|
532
|
+
const v = o[spec.k];
|
|
533
|
+
const multiline = spec.type === "textarea" || (typeof v === "string" && v.includes("\n"));
|
|
534
|
+
const input = multiline
|
|
535
|
+
? h("textarea", { class: `form-control ${spec.mono ? "mono" : ""}`, rows: spec.rows || Math.min(8, Math.max(2, String(v ?? "").split("\n").length)), placeholder: spec.placeholder })
|
|
536
|
+
: h("input", { type: "text", class: `form-control ${spec.mono ? "mono" : ""}`, placeholder: spec.placeholder });
|
|
537
|
+
input.value = v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
538
|
+
let listId = null;
|
|
539
|
+
if (spec.datalist) {
|
|
540
|
+
listId = `dl-${spec.k}-${Math.random().toString(36).slice(2, 7)}`;
|
|
541
|
+
input.setAttribute("list", listId);
|
|
542
|
+
}
|
|
543
|
+
input.addEventListener("input", () => { setKey(o, spec.k, input.value); commitSoon(); });
|
|
544
|
+
input.addEventListener("change", commitNow);
|
|
545
|
+
return fieldWrap(spec, h("div", { class: "sf-ctl" }, input, listId ? h("datalist", { id: listId }, spec.datalist.map((d) => h("option", { value: d }))) : null));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// estrutura (corpo de requisição, polling…) como JSON: só aplica quando é JSON válido; vazio apaga o campo
|
|
549
|
+
function jsonField(o, spec) {
|
|
550
|
+
const text = o[spec.k] == null ? "" : JSON.stringify(o[spec.k], null, 2);
|
|
551
|
+
const ta = h("textarea", { class: "form-control mono", rows: spec.rows || Math.min(10, Math.max(3, text.split("\n").length)), placeholder: spec.placeholder, spellcheck: "false" });
|
|
552
|
+
ta.value = text;
|
|
553
|
+
const err = h("div", { class: "sf-hint sf-error" });
|
|
554
|
+
ta.addEventListener("input", () => {
|
|
555
|
+
const v = ta.value.trim();
|
|
556
|
+
try {
|
|
557
|
+
setKey(o, spec.k, v ? JSON.parse(v) : null);
|
|
558
|
+
ta.classList.remove("invalid"); err.textContent = ""; commitSoon();
|
|
559
|
+
} catch { ta.classList.add("invalid"); err.textContent = "JSON inválido — não aplicado"; }
|
|
560
|
+
});
|
|
561
|
+
ta.addEventListener("change", () => { if (!ta.classList.contains("invalid")) commitNow(); });
|
|
562
|
+
return fieldWrap(spec, h("div", { class: "sf-ctl" }, ta, err));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function numberField(o, spec) {
|
|
566
|
+
const input = h("input", { type: "number", class: "form-control", step: "any", placeholder: spec.placeholder });
|
|
567
|
+
input.value = o[spec.k] ?? "";
|
|
568
|
+
input.addEventListener("input", () => { setKey(o, spec.k, input.value === "" ? null : Number(input.value)); commitSoon(); });
|
|
569
|
+
input.addEventListener("change", commitNow);
|
|
570
|
+
return fieldWrap(spec, input, "sf-narrow");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function boolField(o, spec) {
|
|
574
|
+
const def = spec.default ?? false;
|
|
575
|
+
const input = h("input", { type: "checkbox" });
|
|
576
|
+
input.checked = o[spec.k] ?? def;
|
|
577
|
+
input.addEventListener("change", () => {
|
|
578
|
+
if (input.checked === def) delete o[spec.k];
|
|
579
|
+
else o[spec.k] = input.checked;
|
|
580
|
+
commitNow();
|
|
581
|
+
});
|
|
582
|
+
return h("label", { class: "sf-check" }, input, spec.label);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function selectField(o, spec) {
|
|
586
|
+
// "padrão" com o mesmo texto de uma opção (ex.: Síncrono): essa opção É o padrão, aparece uma vez só e escolhê-la
|
|
587
|
+
// tira o campo do YAML
|
|
588
|
+
const dflt = spec.empty ? spec.options.find(([, l]) => l === spec.empty)?.[0] : undefined;
|
|
589
|
+
const opts = spec.options.map(([v, l]) => h("option", { value: v === dflt ? "" : v, text: l }));
|
|
590
|
+
const sel = h("select", { class: "form-control" }, ...(spec.empty === false || dflt !== undefined ? [] : [h("option", { value: "", text: spec.empty || "—" })]), opts);
|
|
591
|
+
const cur0 = o[spec.k] ?? spec.default;
|
|
592
|
+
const cur = cur0 != null && String(cur0) === String(dflt) ? null : cur0;
|
|
593
|
+
sel.value = cur == null ? "" : String(cur);
|
|
594
|
+
if (cur != null && sel.value !== String(cur)) { // valor fora da lista: mostra mesmo assim
|
|
595
|
+
sel.append(h("option", { value: String(cur), text: String(cur) }));
|
|
596
|
+
sel.value = String(cur);
|
|
597
|
+
}
|
|
598
|
+
// structural: outros campos dependem deste (when), então o formulário se refaz
|
|
599
|
+
sel.addEventListener("change", () => { setKey(o, spec.k, spec.parse ? spec.parse(sel.value) : sel.value); (spec.structural ? commitStructure : commitNow)(); });
|
|
600
|
+
return fieldWrap(spec, sel);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function inferCodeLanguage(filename) {
|
|
604
|
+
const basename = String(filename || "").trim().split(/[\\/]/).pop() || "";
|
|
605
|
+
const dot = basename.lastIndexOf(".");
|
|
606
|
+
if (dot <= 0) return "";
|
|
607
|
+
const extension = basename.slice(dot).toLowerCase();
|
|
608
|
+
return ({ ".py": "Python", ".pyw": "Python", ".java": "Java", ".js": "JavaScript", ".jsx": "JavaScript",
|
|
609
|
+
".mjs": "JavaScript", ".cjs": "JavaScript", ".ts": "TypeScript", ".tsx": "TypeScript", ".mts": "TypeScript",
|
|
610
|
+
".cts": "TypeScript", ".cs": "C#" })[extension] || "";
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function normalizeCodeLanguage(language) {
|
|
614
|
+
const key = String(language || "").trim().toLowerCase().replace(/[\s._-]/g, "");
|
|
615
|
+
return ({ python: "Python", py: "Python", java: "Java", javascript: "JavaScript", js: "JavaScript",
|
|
616
|
+
node: "JavaScript", nodejs: "JavaScript", typescript: "TypeScript", ts: "TypeScript",
|
|
617
|
+
"c#": "C#", csharp: "C#", cs: "C#" })[key] || "";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function codeLanguageField(o, spec, filenameField) {
|
|
621
|
+
const options = ["Python", "Java", "JavaScript", "TypeScript", "C#"];
|
|
622
|
+
const autoOption = h("option", { value: "", text: "Inferir pela extensão" });
|
|
623
|
+
const select = h("select", { class: "form-control" }, autoOption, options.map((language) => h("option", { value: language, text: language })));
|
|
624
|
+
const detect = () => inferCodeLanguage(o[spec.filenameKey]);
|
|
625
|
+
const initialLanguage = normalizeCodeLanguage(o[spec.k]);
|
|
626
|
+
let automatic = !initialLanguage || initialLanguage === detect();
|
|
627
|
+
select.value = automatic ? detect() : initialLanguage;
|
|
628
|
+
const updateAutoLabel = () => {
|
|
629
|
+
const language = detect();
|
|
630
|
+
autoOption.textContent = language ? `Inferir pela extensão · ${language}` : "Inferir pela extensão";
|
|
631
|
+
};
|
|
632
|
+
updateAutoLabel();
|
|
633
|
+
|
|
634
|
+
const filenameInput = filenameField?.querySelector("input, textarea");
|
|
635
|
+
filenameInput?.addEventListener("input", () => {
|
|
636
|
+
if (!automatic) return;
|
|
637
|
+
const language = inferCodeLanguage(filenameInput.value);
|
|
638
|
+
select.value = language;
|
|
639
|
+
setKey(o, spec.k, language || null);
|
|
640
|
+
updateAutoLabel();
|
|
641
|
+
});
|
|
642
|
+
select.addEventListener("change", () => {
|
|
643
|
+
automatic = !select.value;
|
|
644
|
+
setKey(o, spec.k, select.value || null);
|
|
645
|
+
updateAutoLabel();
|
|
646
|
+
(spec.structural ? commitStructure : commitNow)();
|
|
647
|
+
});
|
|
648
|
+
return fieldWrap({ ...spec, hint: "Detectada pela extensão do arquivo; você pode escolher outra linguagem." }, select);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function iconField(o, spec) {
|
|
652
|
+
const input = h("input", { type: "text", class: "form-control", placeholder: "ex.: rocket" });
|
|
653
|
+
input.value = o[spec.k] ?? "";
|
|
654
|
+
input.addEventListener("input", () => { setKey(o, spec.k, input.value); commitSoon(); });
|
|
655
|
+
input.addEventListener("change", commitNow);
|
|
656
|
+
const pick = h("button", { class: "btn-small", type: "button", title: "Escolher na biblioteca" }, "Escolher");
|
|
657
|
+
pick.onclick = () => CTX.pickIcon((name) => { o[spec.k] = name; input.value = name; commitNow(); });
|
|
658
|
+
return fieldWrap(spec, h("div", { class: "sf-inline" }, input, pick));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function numsField(o, spec) {
|
|
662
|
+
const input = h("input", { type: "text", class: "form-control", placeholder: spec.placeholder || "ex.: 1, 3" });
|
|
663
|
+
const cur = o[spec.k];
|
|
664
|
+
input.value = cur == null ? "" : [].concat(cur).map((x) => (x == null ? "" : x)).join(", ");
|
|
665
|
+
const parse = () => {
|
|
666
|
+
const raw = input.value.trim();
|
|
667
|
+
if (!raw) return null;
|
|
668
|
+
const parts = raw.split(/[,;]\s*/).map((x) => (x.trim() === "" ? (spec.allowNull ? null : NaN) : Number(x)));
|
|
669
|
+
return parts.some((x) => Number.isNaN(x)) ? undefined : parts;
|
|
670
|
+
};
|
|
671
|
+
input.addEventListener("input", () => {
|
|
672
|
+
const v = parse();
|
|
673
|
+
input.classList.toggle("invalid", v === undefined);
|
|
674
|
+
if (v !== undefined) { setKey(o, spec.k, v && v.length === 1 && !Array.isArray(cur) && spec.k === "highlight" ? v[0] : v); commitSoon(); }
|
|
675
|
+
});
|
|
676
|
+
input.addEventListener("change", commitNow);
|
|
677
|
+
return fieldWrap(spec, input);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function pairField(o, spec) {
|
|
681
|
+
const cur = Array.isArray(o[spec.k]) ? o[spec.k] : ["", ""];
|
|
682
|
+
const inputs = spec.labels.map((ph, i) => {
|
|
683
|
+
const input = h("input", { type: "text", class: "form-control", placeholder: ph });
|
|
684
|
+
input.value = cur[i] ?? "";
|
|
685
|
+
input.addEventListener("input", () => {
|
|
686
|
+
const v = inputs.map((x) => x.value);
|
|
687
|
+
setKey(o, spec.k, v.every((x) => !x) ? null : v);
|
|
688
|
+
commitSoon();
|
|
689
|
+
});
|
|
690
|
+
input.addEventListener("change", commitNow);
|
|
691
|
+
return input;
|
|
692
|
+
});
|
|
693
|
+
return fieldWrap(spec, h("div", { class: "sf-inline" }, inputs));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function objectField(o, spec, path) {
|
|
697
|
+
// atalho em texto (ex.: problem: "Leva 2 semanas") vira { text: … } sem perder o conteúdo
|
|
698
|
+
if (typeof o[spec.k] === "string" && spec.stringAs) o[spec.k] = { [spec.stringAs]: o[spec.k] };
|
|
699
|
+
if (!o[spec.k] || typeof o[spec.k] !== "object") o[spec.k] = {};
|
|
700
|
+
const target = o[spec.k];
|
|
701
|
+
return h("fieldset", { class: "sf-object" }, h("legend", { text: spec.label }), renderFields(target, spec.fields, `${path}.${spec.k}`));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function moreGroup(o, fields, path) {
|
|
705
|
+
const det = h("details", { class: "sf-more" }, h("summary", { text: "Mais opções" }), renderFields(o, fields, path));
|
|
706
|
+
det.open = openGroups.has(path);
|
|
707
|
+
det.addEventListener("toggle", () => (det.open ? openGroups.add(path) : openGroups.delete(path)));
|
|
708
|
+
return det;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---- listas ----
|
|
712
|
+
function listField(o, spec, path) {
|
|
713
|
+
const key = listKey(o, spec.k);
|
|
714
|
+
const arr = Array.isArray(o[key]) ? o[key] : [];
|
|
715
|
+
const item = spec.item;
|
|
716
|
+
const box = h("div", { class: "sf-list" });
|
|
717
|
+
const head = h("div", { class: "sf-list-head" }, h("span", { class: "sf-label", text: `${spec.label}${arr.length ? ` (${arr.length})` : ""}` }));
|
|
718
|
+
const canAdd = !spec.max || arr.length < spec.max;
|
|
719
|
+
if (canAdd) {
|
|
720
|
+
const add = h("button", { class: "btn-small", type: "button" }, spec.addLabel || "Adicionar");
|
|
721
|
+
add.onclick = () => {
|
|
722
|
+
const next = spec.newItem ? spec.newItem() : item === T || item === TA ? "" : item.textOrObj ? "" : {};
|
|
723
|
+
o[key] = [...arr, next];
|
|
724
|
+
openGroups.add(`${path}.${key}.${arr.length}`);
|
|
725
|
+
commitStructure();
|
|
726
|
+
};
|
|
727
|
+
head.append(add);
|
|
728
|
+
}
|
|
729
|
+
box.append(head);
|
|
730
|
+
arr.forEach((val, i) => box.append(listItem(o, key, arr, i, item, `${path}.${key}.${i}`)));
|
|
731
|
+
return box;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function itemControls(arr, i, o, key) {
|
|
735
|
+
const move = (d) => () => { const j = i + d; if (j < 0 || j >= arr.length) return; [arr[i], arr[j]] = [arr[j], arr[i]]; commitStructure(); };
|
|
736
|
+
const del = () => { arr.splice(i, 1); if (!arr.length) delete o[key]; commitStructure(); };
|
|
737
|
+
return h("div", { class: "sf-item-ctl" },
|
|
738
|
+
h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Subir", onclick: move(-1), disabled: i === 0 || null }, icon("arrow-up")),
|
|
739
|
+
h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Descer", onclick: move(1), disabled: i === arr.length - 1 || null }, icon("arrow-down")),
|
|
740
|
+
h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Remover", onclick: del }, icon("trash-2")));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function listItem(o, key, arr, i, item, path) {
|
|
744
|
+
// item de texto simples: uma linha com os controles
|
|
745
|
+
if (item === T || item === TA) {
|
|
746
|
+
const input = item === TA ? h("textarea", { class: "form-control", rows: 2 }) : h("input", { type: "text", class: "form-control" });
|
|
747
|
+
input.value = arr[i] == null ? "" : String(arr[i]);
|
|
748
|
+
input.addEventListener("input", () => { arr[i] = input.value; commitSoon(); });
|
|
749
|
+
input.addEventListener("change", commitNow);
|
|
750
|
+
return h("div", { class: "sf-item sf-item-simple" }, input, itemControls(arr, i, o, key));
|
|
751
|
+
}
|
|
752
|
+
if (item === "element") return elementCard(arr, i, path, { removable: true, o, key });
|
|
753
|
+
// item objeto (ou texto-ou-objeto: string vira {text} e volta a ser string se só tiver texto)
|
|
754
|
+
const isStr = typeof arr[i] !== "object" || arr[i] == null;
|
|
755
|
+
const target = isStr ? { text: arr[i] == null ? "" : String(arr[i]) } : arr[i];
|
|
756
|
+
if (isStr && !item.textOrObj) arr[i] = target;
|
|
757
|
+
const proxyCommit = () => {
|
|
758
|
+
if (item.textOrObj) {
|
|
759
|
+
const keys = Object.keys(target).filter((k) => !isEmpty(target[k]));
|
|
760
|
+
arr[i] = keys.length === 1 && keys[0] === "text" ? target.text : keys.length ? target : "";
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
return collapsible(path, itemSummary(target, i), [itemControls(arr, i, o, key)], renderFields(target, item.fields, path, proxyCommit));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function itemSummary(t, i) {
|
|
767
|
+
const s = t.title || t.text || t.label || t.when || t.value || t.name || t.icon || "";
|
|
768
|
+
return `${i + 1}. ${String(s).replace(/==|\*\*|\^\^|~~|`/g, "").slice(0, 60) || "(vazio)"}`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ---- elementos ----
|
|
772
|
+
function elementField(o, spec, path) {
|
|
773
|
+
const cur = o[spec.k];
|
|
774
|
+
const box = h("div", { class: "sf-list" }, h("div", { class: "sf-list-head" }, h("span", { class: "sf-label", text: spec.label })));
|
|
775
|
+
if (cur == null) {
|
|
776
|
+
box.firstChild.append(kindMenu("Adicionar", (tpl) => { o[spec.k] = tpl; openGroups.add(`${path}.${spec.k}`); commitStructure(); }));
|
|
777
|
+
return box;
|
|
778
|
+
}
|
|
779
|
+
// atalhos em texto: figura "foto.jpg" = imagem; nota/lado "texto" = texto
|
|
780
|
+
if (typeof cur !== "object") o[spec.k] = spec.stringAs === "text" ? { lead: String(cur) } : { image: String(cur) };
|
|
781
|
+
const holder = [o[spec.k]];
|
|
782
|
+
box.append(elementCard(holder, 0, `${path}.${spec.k}`, {
|
|
783
|
+
removable: true, onRemove: () => { delete o[spec.k]; commitStructure(); },
|
|
784
|
+
onReplace: (v) => { o[spec.k] = v; }, positioned: spec.positioned,
|
|
785
|
+
}));
|
|
786
|
+
return box;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function elementsField(o, spec, path) {
|
|
790
|
+
let arr = o[spec.k];
|
|
791
|
+
if (arr != null && !Array.isArray(arr)) arr = o[spec.k] = [arr];
|
|
792
|
+
arr = arr || [];
|
|
793
|
+
const box = h("div", { class: "sf-list" });
|
|
794
|
+
const head = h("div", { class: "sf-list-head" }, h("span", { class: "sf-label", text: `${spec.label}${arr.length ? ` (${arr.length})` : ""}` }),
|
|
795
|
+
kindMenu("Adicionar", (tpl) => {
|
|
796
|
+
if (spec.positioned) Object.assign(tpl, { x: 160, y: 160 });
|
|
797
|
+
o[spec.k] = [...arr, tpl];
|
|
798
|
+
openGroups.add(`${path}.${spec.k}.${arr.length}`);
|
|
799
|
+
commitStructure();
|
|
800
|
+
}));
|
|
801
|
+
box.append(head);
|
|
802
|
+
arr.forEach((_, i) => box.append(elementCard(arr, i, `${path}.${spec.k}.${i}`, { removable: true, o, key: spec.k, positioned: spec.positioned })));
|
|
803
|
+
return box;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function kindMenu(label, onPick) {
|
|
807
|
+
const sel = h("select", { class: "form-control form-control-sm sf-kind-add", title: label },
|
|
808
|
+
h("option", { value: "", text: `${label}…` }), ELEMENT_KINDS.map((k) => h("option", { value: k.id, text: k.label })));
|
|
809
|
+
sel.addEventListener("change", () => {
|
|
810
|
+
const k = ELEMENT_KINDS.find((x) => x.id === sel.value);
|
|
811
|
+
if (k) onPick(k.tpl());
|
|
812
|
+
});
|
|
813
|
+
return sel;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function elementCard(arr, i, path, opts = {}) {
|
|
817
|
+
let e = arr[i];
|
|
818
|
+
if (typeof e !== "object" || e == null) e = arr[i] = { body: e == null ? "" : String(e) };
|
|
819
|
+
const kind = kindOf(e);
|
|
820
|
+
// trocar o tipo: começa do modelo do novo tipo, mantendo posição/clique
|
|
821
|
+
const kindSel = h("select", { class: "form-control form-control-sm", title: "Tipo do elemento" },
|
|
822
|
+
ELEMENT_KINDS.map((k) => h("option", { value: k.id, text: k.label })), kind ? null : h("option", { value: "", text: "Desconhecido" }));
|
|
823
|
+
kindSel.value = kind ? kind.id : "";
|
|
824
|
+
kindSel.addEventListener("change", () => {
|
|
825
|
+
const k = ELEMENT_KINDS.find((x) => x.id === kindSel.value);
|
|
826
|
+
if (!k) return;
|
|
827
|
+
const keep = Object.fromEntries(EL_COMMON_KEYS.filter((c) => e[c] != null).map((c) => [c, e[c]]));
|
|
828
|
+
const next = { ...k.tpl(), ...keep };
|
|
829
|
+
arr[i] = next;
|
|
830
|
+
opts.onReplace?.(next);
|
|
831
|
+
openGroups.add(path);
|
|
832
|
+
commitStructure();
|
|
833
|
+
});
|
|
834
|
+
const ctl = [];
|
|
835
|
+
if (opts.removable) {
|
|
836
|
+
if (opts.onRemove) ctl.push(h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Remover", onclick: opts.onRemove }, icon("trash-2")));
|
|
837
|
+
else ctl.push(itemControls(arr, i, opts.o, opts.key));
|
|
838
|
+
}
|
|
839
|
+
const body = h("div", { class: "sf-stack" }); // o collapsible já dá o .sf-item-body
|
|
840
|
+
if (!kind) body.append(h("div", { class: "sf-hint", text: "Tipo não reconhecido — edite os campos abaixo." }));
|
|
841
|
+
else if (kind.text) body.append(textElementFields(e));
|
|
842
|
+
else if (kind.chart) body.append(chartFields(e, path, "chart"));
|
|
843
|
+
else body.append(renderFields(e, kind.fields, path));
|
|
844
|
+
body.append(moreGroup(e, EL_MORE(opts.positioned), `${path}.more`));
|
|
845
|
+
const used = new Set([...(kind?.fields || []).flatMap((s) => [].concat(s.k || [])), ...EL_COMMON_KEYS, ...(kind?.text ? ["text", "as", "size", "weight", "upper", "face", "lh", "fit", ...TEXT_KEYS] : []),
|
|
846
|
+
...(kind?.chart ? chartKeys(e) : [])]);
|
|
847
|
+
const extra = genericFields(e, used, `${path}.other`);
|
|
848
|
+
if (extra) body.append(extra);
|
|
849
|
+
return collapsible(path, elementSummary(e, kind), [kindSel, ...ctl], body, "sf-element");
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function elementSummary(e, kind) {
|
|
853
|
+
const role = textRole(e);
|
|
854
|
+
const s = e.text ?? (role ? e[role] : null) ?? e.icon ?? e.picto ?? e.image ?? e.badge ?? e.label ?? e.title ?? "";
|
|
855
|
+
return String(s).replace(/==|\*\*|\^\^|~~|`/g, "").slice(0, 40) || kind?.label || "Elemento";
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Texto: { h2: "…" } ou { text: "…", as: h2 } — o estilo troca a chave (ou o `as`) sem perder o texto
|
|
859
|
+
function textElementFields(e) {
|
|
860
|
+
const frag = document.createDocumentFragment();
|
|
861
|
+
const getRole = () => (e.text != null ? e.as || "body" : textRole(e) || "body");
|
|
862
|
+
const ta = h("textarea", { class: "form-control", rows: 2 });
|
|
863
|
+
ta.value = e.text != null ? e.text : e[textRole(e)] ?? "";
|
|
864
|
+
ta.addEventListener("input", () => {
|
|
865
|
+
if (e.text != null) e.text = ta.value;
|
|
866
|
+
else e[textRole(e) || "body"] = ta.value;
|
|
867
|
+
commitSoon();
|
|
868
|
+
});
|
|
869
|
+
ta.addEventListener("change", commitNow);
|
|
870
|
+
frag.append(fieldWrap({ label: "Texto" }, ta));
|
|
871
|
+
const sel = h("select", { class: "form-control" }, TEXT_ROLES.map(([v, l]) => h("option", { value: v, text: l })));
|
|
872
|
+
sel.value = getRole();
|
|
873
|
+
sel.addEventListener("change", () => {
|
|
874
|
+
if (e.text != null) e.as = sel.value;
|
|
875
|
+
else { const r = textRole(e); const v = e[r]; delete e[r]; e[sel.value] = v; }
|
|
876
|
+
commitNow();
|
|
877
|
+
});
|
|
878
|
+
frag.append(fieldWrap({ label: "Estilo" }, sel));
|
|
879
|
+
frag.append(renderFields(e, [f.num("size", "Tamanho (px)"), f.num("weight", "Peso da fonte")], ""));
|
|
880
|
+
return frag;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ---- gráficos ----
|
|
884
|
+
const CHART_MORE = [f.text("prefix", "Antes do valor"), f.num("decimals", "Casas decimais"), f.bool("legend", "Legenda"), f.num("fontSize", "Tamanho do texto (px)"),
|
|
885
|
+
f.num("valueSize", "Tamanho dos valores (px)"), f.num("labelWidth", "Largura dos rótulos (px)"), f.num("barHeight", "Altura das barras (px)"), f.num("barWidth", "Largura das barras (px)"),
|
|
886
|
+
f.num("thickness", "Espessura (rosca)"), f.num("centerSize", "Tamanho do centro (px)"), f.color("color", "Cor"), f.num("cw", "Largura do gráfico (px)"), f.num("ch", "Altura do gráfico (px)")];
|
|
887
|
+
function chartKeys(c) {
|
|
888
|
+
const type = c.chart || c.type || "bar";
|
|
889
|
+
return ["chart", "type", ...(CHART_FIELDS[type] || []).flatMap((s) => [].concat(s.k)), ...CHART_MORE.map((s) => s.k)];
|
|
890
|
+
}
|
|
891
|
+
function chartFields(c, path, typeKey) {
|
|
892
|
+
const frag = document.createDocumentFragment();
|
|
893
|
+
const tk = c.type != null && c.chart == null ? "type" : typeKey;
|
|
894
|
+
const sel = h("select", { class: "form-control" }, CHART_TYPES.map(([v, l]) => h("option", { value: v, text: l })));
|
|
895
|
+
sel.value = c[tk] || "bar";
|
|
896
|
+
sel.addEventListener("change", () => { c[tk] = sel.value; commitStructure(); });
|
|
897
|
+
frag.append(fieldWrap({ label: "Tipo de gráfico" }, sel));
|
|
898
|
+
frag.append(renderFields(c, CHART_FIELDS[c[tk] || "bar"] || [], `${path}.chart`));
|
|
899
|
+
frag.append(moreGroup(c, CHART_MORE, `${path}.chart.more`));
|
|
900
|
+
return frag;
|
|
901
|
+
}
|
|
902
|
+
function chartField(o, spec, path) {
|
|
903
|
+
if (!o[spec.k] || typeof o[spec.k] !== "object") o[spec.k] = { chart: "bar", data: [] };
|
|
904
|
+
const c = o[spec.k];
|
|
905
|
+
const fs = h("fieldset", { class: "sf-object" }, h("legend", { text: spec.label }), chartFields(c, path, "chart"));
|
|
906
|
+
const extra = genericFields(c, new Set(chartKeys(c)), `${path}.chart.other`);
|
|
907
|
+
if (extra) fs.append(extra);
|
|
908
|
+
return fs;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ---- planilha do gráfico de dados: Rótulo + uma coluna por série; cola do Excel como no Excel ----
|
|
912
|
+
// Uma série: data: [{label, value}] (ou parts, na rosca). Várias: labels + series [{name, values}] (linhas, barras e
|
|
913
|
+
// colunas agrupadas). Colar em qualquer célula preenche a partir dela; a primeira linha com texto vira o nome das séries.
|
|
914
|
+
const cellNum = (c) => {
|
|
915
|
+
const t = String(c ?? "").trim().replace(/\s/g, "").replace(/^R\$/, "").replace(/%$/, "");
|
|
916
|
+
if (!t) return null;
|
|
917
|
+
const n = Number(/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(t) ? t.replace(/\./g, "").replace(",", ".") : t.replace(",", "."));
|
|
918
|
+
return Number.isFinite(n) ? n : NaN;
|
|
919
|
+
};
|
|
920
|
+
const parseCells = (text) => {
|
|
921
|
+
const lines = String(text ?? "").replace(/\r/g, "").split("\n");
|
|
922
|
+
while (lines.length && !lines.at(-1).trim()) lines.pop();
|
|
923
|
+
const sep = lines.some((l) => l.includes("\t")) ? "\t" : lines.some((l) => l.includes(";")) ? ";" : ",";
|
|
924
|
+
return lines.map((l) => l.split(sep).map((c) => c.trim().replace(/^"(.*)"$/, "$1")));
|
|
925
|
+
};
|
|
926
|
+
function sheetRead(c, spec) {
|
|
927
|
+
const key = spec.key || "data";
|
|
928
|
+
if (Array.isArray(c.series) && Array.isArray(c.labels) && !spec.single)
|
|
929
|
+
return { names: c.series.map((x, i) => x.name ?? `Série ${i + 1}`), rows: c.labels.map((l, i) => [String(l ?? ""), ...c.series.map((x) => x.values?.[i] ?? null)]), colors: c.series.map((x) => x.color) };
|
|
930
|
+
const d = (Array.isArray(c[key]) ? c[key] : []).map((x) => (typeof x === "number" ? { label: "", value: x } : x || {}));
|
|
931
|
+
return { names: [spec.single ? "Valor" : c.seriesName || "Valor"], rows: d.map((x) => [String(x.label ?? ""), x.value ?? null]), colors: [], itemColors: d.map((x) => x.color) };
|
|
932
|
+
}
|
|
933
|
+
function sheetWrite(c, spec, t) {
|
|
934
|
+
const key = spec.key || "data";
|
|
935
|
+
const rows = t.rows.filter((r) => String(r[0] ?? "").trim() || r.slice(1).some((v) => v != null));
|
|
936
|
+
const multi = !spec.single && (spec.multi || t.names.length > 1);
|
|
937
|
+
delete c.data; delete c.parts; delete c.values;
|
|
938
|
+
if (multi) {
|
|
939
|
+
c.labels = rows.map((r) => r[0]);
|
|
940
|
+
c.series = t.names.map((name, j) => ({ name, values: rows.map((r) => r[j + 1] ?? null), ...(t.colors?.[j] ? { color: t.colors[j] } : {}) }));
|
|
941
|
+
} else {
|
|
942
|
+
delete c.labels; delete c.series;
|
|
943
|
+
c[key] = rows.map((r, i) => ({ label: r[0], value: r[1] ?? 0, ...(t.itemColors?.[i] ? { color: t.itemColors[i] } : {}) }));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Tabela de texto como no Excel: a primeira linha é o cabeçalho; Enter/setas andam; a última linha em branco cria a
|
|
947
|
+
// próxima; colar do Excel (tab) ou de um CSV preenche a partir da célula; + coluna, − linha/coluna
|
|
948
|
+
function tableGridField(o, spec) {
|
|
949
|
+
const cols = Math.max(1, (o.head || []).length, ...(o.rows || []).map((r) => (Array.isArray(r) ? r.length : 0)));
|
|
950
|
+
const grid = [[...(o.head || [])], ...(o.rows || []).map((r) => (Array.isArray(r) ? [...r] : [r]))].map((r) => Array.from({ length: cols }, (_, j) => (r[j] == null ? "" : String(r[j]))));
|
|
951
|
+
if (!grid.length) grid.push(Array(cols).fill(""));
|
|
952
|
+
grid.push(Array(cols).fill(""));
|
|
953
|
+
const wrapEl = h("div", { class: "sf-sheet-wrap sf-tgrid" });
|
|
954
|
+
const save = (structure) => {
|
|
955
|
+
const rows = grid.filter((r, i) => i === 0 || r.some((v) => String(v).trim()));
|
|
956
|
+
o.head = rows[0];
|
|
957
|
+
o.rows = rows.slice(1).map((r) => r.map((v) => (/^-?\d+([.,]\d+)?$/.test(String(v).trim()) && !/^0\d/.test(String(v).trim()) ? String(v).trim() : v)));
|
|
958
|
+
if (!o.head.some((v) => String(v).trim())) delete o.head;
|
|
959
|
+
structure ? commitStructure() : commitSoon();
|
|
960
|
+
};
|
|
961
|
+
const focusCell = (i, j) => { const el = wrapEl.querySelector(`[data-tcell="${i},${j}"]`); if (el) { el.focus(); el.select(); } };
|
|
962
|
+
const draw = () => {
|
|
963
|
+
const n = grid[0].length;
|
|
964
|
+
const top = h("tr", {}, ...grid[0].map((_, j) => h("th", { class: "sf-sheet-act" }, n > 1 ? h("button", { class: "icon-btn icon-btn-sm", type: "button", title: `Remover a coluna ${j + 1}`, onclick: () => { grid.forEach((r) => r.splice(j, 1)); save(true); draw(); } }, icon("x")) : null)),
|
|
965
|
+
h("th", { class: "sf-sheet-act" }, h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Adicionar coluna", "data-tgrid-addcol": "", onclick: () => { grid.forEach((r) => r.push("")); save(true); draw(); } }, icon("plus"))));
|
|
966
|
+
const body = grid.map((r, i) => h("tr", { class: i === 0 ? "sf-tgrid-head" : "" }, ...r.map((v, j) => {
|
|
967
|
+
const inp = h("input", { class: "sf-sheet-in", value: v, "data-tcell": `${i},${j}`, "aria-label": i === 0 ? `Título da coluna ${j + 1}` : `Linha ${i}, coluna ${j + 1}`, placeholder: i === 0 ? `Coluna ${j + 1}` : "", spellcheck: "false" });
|
|
968
|
+
inp.addEventListener("input", () => {
|
|
969
|
+
r[j] = inp.value;
|
|
970
|
+
if (i === grid.length - 1 && inp.value.trim()) { grid.push(Array(r.length).fill("")); save(false); draw(); const el = wrapEl.querySelector(`[data-tcell="${i},${j}"]`); el?.focus(); el?.setSelectionRange(el.value.length, el.value.length); return; }
|
|
971
|
+
save(false);
|
|
972
|
+
});
|
|
973
|
+
inp.addEventListener("keydown", (e) => {
|
|
974
|
+
if (e.key === "Enter" || e.key === "ArrowDown") { e.preventDefault(); focusCell(i + 1, j); }
|
|
975
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); focusCell(i - 1, j); }
|
|
976
|
+
});
|
|
977
|
+
inp.addEventListener("paste", (e) => {
|
|
978
|
+
const text = e.clipboardData?.getData("text/plain") || "";
|
|
979
|
+
if (!/[\t\n;]/.test(text)) return;
|
|
980
|
+
const cells = parseCells(text);
|
|
981
|
+
if (!cells.length) return;
|
|
982
|
+
e.preventDefault();
|
|
983
|
+
const need = j + Math.max(...cells.map((c) => c.length));
|
|
984
|
+
grid.forEach((row) => { while (row.length < need) row.push(""); });
|
|
985
|
+
cells.forEach((c, k) => { while (grid.length <= i + k + 1) grid.push(Array(grid[0].length).fill("")); c.forEach((val, q) => { grid[i + k][j + q] = String(val ?? ""); }); });
|
|
986
|
+
save(true); draw();
|
|
987
|
+
});
|
|
988
|
+
return h("td", {}, inp);
|
|
989
|
+
}), h("td", { class: "sf-sheet-act" }, i > 0 && grid.length > 2 ? h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Remover linha", onclick: () => { grid.splice(i, 1); save(true); draw(); } }, icon("trash-2")) : null)));
|
|
990
|
+
wrapEl.replaceChildren(h("table", { class: "sf-sheet" }, h("thead", {}, top), h("tbody", {}, body)));
|
|
991
|
+
CTX.hydrate?.(wrapEl);
|
|
992
|
+
};
|
|
993
|
+
draw();
|
|
994
|
+
return fieldWrap(spec, wrapEl, "sf-sheet-field");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function sheetField(c, spec) {
|
|
998
|
+
const t = sheetRead(c, spec);
|
|
999
|
+
t.rows.push(["", ...t.names.map(() => null)]); // sempre uma linha em branco no fim: digitar nela cria a próxima
|
|
1000
|
+
const canMulti = !spec.single;
|
|
1001
|
+
const wrapEl = h("div", { class: "sf-sheet-wrap" });
|
|
1002
|
+
const status = h("div", { class: "sf-hint", "data-sheet-status": "" });
|
|
1003
|
+
const save = (structure) => { sheetWrite(c, spec, t); structure ? commitStructure() : commitSoon(); };
|
|
1004
|
+
const draw = () => {
|
|
1005
|
+
const width = t.names.length + 1;
|
|
1006
|
+
t.rows.forEach((r) => { while (r.length < width) r.push(null); r.length = width; });
|
|
1007
|
+
const head = h("tr", {}, h("th", { class: "sf-sheet-corner", text: "Rótulo" }), ...t.names.map((name, j) => {
|
|
1008
|
+
const inp = h("input", { class: "sf-sheet-in sf-sheet-name", value: name, "aria-label": `Nome da série ${j + 1}`, "data-sheet-name": String(j), spellcheck: "false" });
|
|
1009
|
+
inp.addEventListener("input", () => { t.names[j] = inp.value; save(false); });
|
|
1010
|
+
const del = t.names.length > 1 ? h("button", { class: "icon-btn icon-btn-sm", type: "button", title: `Remover a série ${name}`, onclick: () => { t.names.splice(j, 1); t.colors?.splice(j, 1); t.rows.forEach((r) => r.splice(j + 1, 1)); save(true); } }, icon("x")) : null;
|
|
1011
|
+
return h("th", {}, h("div", { class: "sf-sheet-th" }, inp, del));
|
|
1012
|
+
}), h("th", { class: "sf-sheet-act" }));
|
|
1013
|
+
const body = t.rows.map((r, i) => h("tr", {}, ...r.map((v, j) => {
|
|
1014
|
+
const inp = h("input", { class: `sf-sheet-in${j ? " num" : ""}`, value: v == null ? "" : j ? String(v).replace(".", ",") : v, "aria-label": j ? `${t.names[j - 1]}, linha ${i + 1}` : `Rótulo da linha ${i + 1}`, "data-cell": `${i},${j}`, spellcheck: "false", inputmode: j ? "decimal" : "text" });
|
|
1015
|
+
inp.addEventListener("input", () => {
|
|
1016
|
+
if (j === 0) r[0] = inp.value;
|
|
1017
|
+
else { const n = cellNum(inp.value); inp.classList.toggle("invalid", Number.isNaN(n)); if (Number.isNaN(n)) return; r[j] = n; }
|
|
1018
|
+
if (i === t.rows.length - 1 && inp.value.trim()) { t.rows.push(Array(r.length).fill(null)); t.rows.at(-1)[0] = ""; save(false); draw(); focusCell(i, j, false); return; }
|
|
1019
|
+
save(false);
|
|
1020
|
+
});
|
|
1021
|
+
inp.addEventListener("keydown", (e) => {
|
|
1022
|
+
if (e.key === "Enter") { e.preventDefault(); focusCell(i + 1, j); }
|
|
1023
|
+
else if (e.key === "ArrowDown") { e.preventDefault(); focusCell(i + 1, j); }
|
|
1024
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); focusCell(i - 1, j); }
|
|
1025
|
+
});
|
|
1026
|
+
return h("td", {}, inp);
|
|
1027
|
+
}), h("td", { class: "sf-sheet-act" }, t.rows.length > 1 ? h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Remover linha", onclick: () => { t.rows.splice(i, 1); save(true); } }, icon("trash-2")) : null)));
|
|
1028
|
+
const table = h("table", { class: "sf-sheet" }, h("thead", {}, head), h("tbody", {}, body));
|
|
1029
|
+
wrapEl.replaceChildren(table);
|
|
1030
|
+
CTX.hydrate?.(wrapEl);
|
|
1031
|
+
};
|
|
1032
|
+
const focusCell = (i, j, select = true) => { const el = wrapEl.querySelector(`[data-cell="${i},${j}"]`); if (!el) return; el.focus(); if (select) el.select(); else el.setSelectionRange(el.value.length, el.value.length); };
|
|
1033
|
+
// colar do Excel: a partir da célula onde a pessoa colou (no cabeçalho, com nomes das séries)
|
|
1034
|
+
const paste = (text, i0, j0) => {
|
|
1035
|
+
const cells = parseCells(text);
|
|
1036
|
+
if (!cells.length) return false;
|
|
1037
|
+
let start = 0;
|
|
1038
|
+
const valueCols = cells[0].slice(j0 === 0 ? 1 : 0);
|
|
1039
|
+
if (i0 === 0 && valueCols.length && valueCols.every((v) => v === "" || Number.isNaN(cellNum(v)) || cellNum(v) == null) && cells.length > 1) {
|
|
1040
|
+
// primeira linha é cabeçalho: nomes das séries
|
|
1041
|
+
const names = valueCols.map((v, k) => v || `Série ${k + 1}`);
|
|
1042
|
+
const need = (j0 === 0 ? 0 : j0 - 1) + names.length;
|
|
1043
|
+
if (canMulti) while (t.names.length < need) t.names.push(`Série ${t.names.length + 1}`);
|
|
1044
|
+
names.forEach((n, k) => { const at = (j0 === 0 ? 0 : j0 - 1) + k; if (at < t.names.length) t.names[at] = n; });
|
|
1045
|
+
start = 1;
|
|
1046
|
+
}
|
|
1047
|
+
const body = cells.slice(start);
|
|
1048
|
+
const width = Math.max(...body.map((r) => r.length));
|
|
1049
|
+
const needCols = j0 + width - 1;
|
|
1050
|
+
if (canMulti) while (t.names.length < needCols) t.names.push(`Série ${t.names.length + 1}`);
|
|
1051
|
+
let dropped = 0;
|
|
1052
|
+
body.forEach((row, k) => {
|
|
1053
|
+
const i = i0 + k;
|
|
1054
|
+
while (t.rows.length <= i) t.rows.push(Array(t.names.length + 1).fill(null).map((v, q) => (q ? null : "")));
|
|
1055
|
+
row.forEach((val, q) => {
|
|
1056
|
+
const j = j0 + q;
|
|
1057
|
+
if (j > t.names.length) { dropped++; return; }
|
|
1058
|
+
if (j === 0) t.rows[i][0] = val;
|
|
1059
|
+
else { const n = cellNum(val); t.rows[i][j] = Number.isNaN(n) ? null : n; }
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
// valores com % colados e sem unidade: a unidade vira %
|
|
1063
|
+
if (!c.suffix && /%/.test(text) && spec.key !== "parts") c.suffix = "%";
|
|
1064
|
+
t.rows = t.rows.filter((r, idx) => idx === t.rows.length - 1 || String(r[0] ?? "").trim() || r.slice(1).some((v) => v != null));
|
|
1065
|
+
if (String(t.rows.at(-1)[0] ?? "").trim() || t.rows.at(-1).slice(1).some((v) => v != null)) t.rows.push(Array(t.names.length + 1).fill(null).map((v, q) => (q ? null : "")));
|
|
1066
|
+
status.textContent = dropped ? `Este tipo de gráfico usa uma coluna de valores: ${dropped} célula(s) ficaram de fora. Linhas ou Colunas aceitam várias séries.` : `Colado: ${body.length} linha(s).`;
|
|
1067
|
+
save(true);
|
|
1068
|
+
return true;
|
|
1069
|
+
};
|
|
1070
|
+
wrapEl.addEventListener("paste", (e) => {
|
|
1071
|
+
const text = e.clipboardData?.getData("text/plain") || "";
|
|
1072
|
+
if (!/[\t\n;]/.test(text.trim())) return; // um valor só: cola normal na célula
|
|
1073
|
+
const cell = e.target.closest("[data-cell]"), name = e.target.closest("[data-sheet-name]");
|
|
1074
|
+
const [i0, j0] = cell ? cell.dataset.cell.split(",").map(Number) : name ? [0, Number(name.dataset.sheetName) + 1] : [0, 0];
|
|
1075
|
+
e.preventDefault();
|
|
1076
|
+
paste(text, i0, j0);
|
|
1077
|
+
});
|
|
1078
|
+
draw();
|
|
1079
|
+
const file = h("input", { type: "file", accept: ".csv,.tsv,.txt,text/csv", hidden: true });
|
|
1080
|
+
file.addEventListener("change", async () => {
|
|
1081
|
+
const text = await file.files[0]?.text(); if (text == null) return;
|
|
1082
|
+
t.rows = [["", null]]; t.names = [t.names[0] || "Valor"];
|
|
1083
|
+
paste(text, 0, 0);
|
|
1084
|
+
});
|
|
1085
|
+
const tools = h("div", { class: "sf-sheet-tools" },
|
|
1086
|
+
canMulti ? h("button", { class: "btn btn-small", type: "button", "data-sheet-add-series": "", onclick: () => { t.names.push(`Série ${t.names.length + 1}`); t.rows.forEach((r) => r.push(null)); save(true); } }, icon("plus"), " Série") : null,
|
|
1087
|
+
h("button", { class: "btn btn-small", type: "button", onclick: () => file.click() }, icon("upload"), " Importar CSV"), file);
|
|
1088
|
+
return fieldWrap({ label: spec.label, hint: "Copie as células no Excel (ou Google Planilhas) e cole aqui com Ctrl+V. A primeira coluna é o rótulo; cada coluna de números é uma série." }, h("div", { class: "sf-ctl" }, wrapEl, tools, status));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// ---- foto de um item (carrossel): miniatura + Escolher foto; sem foto, o slide usa uma de demonstração ----
|
|
1092
|
+
function photoField(o, spec) {
|
|
1093
|
+
const cur = o[spec.k];
|
|
1094
|
+
const thumb = cur ? h("img", { class: "sf-photo-thumb", src: /^(data:|https?:)/.test(cur) ? cur : "", alt: "" }) : h("span", { class: "sf-photo-empty", text: "de demonstração" });
|
|
1095
|
+
const pick = h("button", { class: "btn btn-small", type: "button", "data-photo-pick": "", onclick: () => CTX.chooseImage?.(o) }, icon("image"), cur ? " Trocar foto" : " Escolher foto");
|
|
1096
|
+
const clear = cur ? h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Voltar para a foto de demonstração", onclick: () => { delete o[spec.k]; delete o.alt; commitStructure(); } }, icon("trash-2")) : null;
|
|
1097
|
+
return fieldWrap(spec, h("div", { class: "sf-ctl sf-photo" }, thumb, pick, clear));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ---- linhas de um arquivo: {2: "texto", 5: null} editado como "2: texto" por linha; ours/theirs/both passam direto ----
|
|
1101
|
+
function lineMapField(o, spec) {
|
|
1102
|
+
const cur = o[spec.k];
|
|
1103
|
+
const toText = (v) => (typeof v === "string" ? v : v && typeof v === "object" ? Object.entries(v).map(([n, t]) => `${n}: ${t === null ? "(apagar)" : t}`).join("\n") : "");
|
|
1104
|
+
const ta = h("textarea", { class: "form-control mono", rows: 2, spellcheck: "false", placeholder: "2: novo texto da linha 2" });
|
|
1105
|
+
ta.value = toText(cur);
|
|
1106
|
+
const err = h("div", { class: "sf-hint sf-error" });
|
|
1107
|
+
ta.addEventListener("input", () => {
|
|
1108
|
+
const raw = ta.value.trim();
|
|
1109
|
+
if (!raw) { delete o[spec.k]; err.textContent = ""; commitSoon(); return; }
|
|
1110
|
+
if (/^(ours|theirs|both)$/i.test(raw)) { o[spec.k] = raw.toLowerCase(); err.textContent = ""; commitSoon(); return; }
|
|
1111
|
+
const map = {}, bad = [];
|
|
1112
|
+
raw.split("\n").forEach((l) => { const m = l.match(/^\s*(\d+)\s*:\s?(.*)$/); if (!m) { if (l.trim()) bad.push(l.trim()); return; } map[m[1]] = /^\(apagar\)$/i.test(m[2].trim()) ? null : m[2]; });
|
|
1113
|
+
err.textContent = bad.length ? `Use o número da linha e dois pontos: 2: texto ("${bad[0]}")` : "";
|
|
1114
|
+
if (!bad.length) { o[spec.k] = map; commitSoon(); }
|
|
1115
|
+
});
|
|
1116
|
+
ta.addEventListener("change", () => { if (!err.textContent) commitNow(); });
|
|
1117
|
+
return fieldWrap(spec, h("div", { class: "sf-ctl" }, ta, err));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ---- gráfico de fórmulas (slide science): escreve a*sin(b*x) e plota; cada letra vira controle deslizante ----
|
|
1121
|
+
const PLOT_PRESETS = { wave: { functions: ["sin(x)"], x: [-6.3, 6.3] }, parabola: { functions: ["x^2"], x: [-4, 4] }, surface: { surface: "sin(sqrt(x^2+y^2))", x: [-4, 4] } };
|
|
1122
|
+
function plotField(o, spec) {
|
|
1123
|
+
if (!o[spec.k] || typeof o[spec.k] !== "object") o[spec.k] = {};
|
|
1124
|
+
const pl = o[spec.k];
|
|
1125
|
+
// exemplo antigo (preset) vira fórmula editável
|
|
1126
|
+
if (pl.preset && PLOT_PRESETS[pl.preset]) { const pre = PLOT_PRESETS[pl.preset]; delete pl.preset; for (const [k, v] of Object.entries(pre)) if (pl[k] == null) pl[k] = v; }
|
|
1127
|
+
const F = window.SagaFormula;
|
|
1128
|
+
const check = (src, vars) => { try { return { params: F.compile(src, vars).params }; } catch (e) { return { error: e.message }; } };
|
|
1129
|
+
const box = h("fieldset", { class: "sf-object sf-plot" }, h("legend", { text: spec.label }));
|
|
1130
|
+
pl.functions = [].concat(pl.functions ?? []).map((x) => (typeof x === "string" ? x : x?.fn ?? ""));
|
|
1131
|
+
// fórmulas: uma por linha; o erro aparece embaixo enquanto digita
|
|
1132
|
+
const list = h("div", { class: "sf-plot-fns" });
|
|
1133
|
+
const drawFns = () => {
|
|
1134
|
+
list.replaceChildren(...pl.functions.map((src, i) => {
|
|
1135
|
+
const input = h("input", { type: "text", class: "form-control mono", placeholder: "a*sin(b*x)", spellcheck: "false", "aria-label": `Fórmula ${i + 1}`, "data-plot-fn": String(i) });
|
|
1136
|
+
input.value = src;
|
|
1137
|
+
const err = h("div", { class: "sf-hint sf-error" });
|
|
1138
|
+
const validate = () => { const r = input.value.trim() ? check(input.value, ["x"]) : {}; err.textContent = r.error ? `Não entendi: ${r.error}` : ""; input.classList.toggle("invalid", !!r.error); return r; };
|
|
1139
|
+
validate();
|
|
1140
|
+
input.addEventListener("input", () => { pl.functions[i] = input.value; if (!validate().error) commitSoon(); });
|
|
1141
|
+
// ao sair do campo: letra nova vira controle deslizante (o formulário se refaz)
|
|
1142
|
+
input.addEventListener("change", () => { pl.functions = pl.functions.filter((x) => x.trim()); commitStructure(); });
|
|
1143
|
+
const del = h("button", { class: "icon-btn icon-btn-sm", type: "button", title: "Remover fórmula", onclick: () => { pl.functions.splice(i, 1); commitStructure(); } }, icon("trash-2"));
|
|
1144
|
+
return h("div", { class: "sf-plot-fn" }, h("span", { class: "sf-plot-y mono", text: "y =" }), input, del, err);
|
|
1145
|
+
}));
|
|
1146
|
+
CTX.hydrate?.(list);
|
|
1147
|
+
};
|
|
1148
|
+
drawFns();
|
|
1149
|
+
const add = h("button", { class: "btn btn-small", type: "button", "data-plot-add": "", onclick: () => { pl.functions.push(""); drawFns(); list.lastElementChild?.querySelector("input")?.focus(); } }, icon("plus"), " Adicionar fórmula");
|
|
1150
|
+
box.append(fieldWrap({ label: "Fórmulas", hint: "Ex.: a*sin(b*x), x^2 - 2x + 1, e^(-x^2). Letra que não é x vira controle deslizante na apresentação." }, h("div", { class: "sf-ctl" }, list, add)));
|
|
1151
|
+
const range = pairField(pl, { k: "x", label: "Intervalo de x", labels: ["de (-10)", "até (10)"] });
|
|
1152
|
+
range.addEventListener("input", () => { if (Array.isArray(pl.x)) pl.x = pl.x.map((v) => (String(v).trim() === "" ? "" : Number(String(v).replace(",", ".")))); });
|
|
1153
|
+
box.append(range);
|
|
1154
|
+
// parâmetros: os que aparecem nas fórmulas; valor inicial, mínimo, máximo e nome
|
|
1155
|
+
const names = new Set();
|
|
1156
|
+
pl.functions.forEach((src) => { if (src.trim()) (check(src, ["x"]).params || []).forEach((n) => names.add(n)); });
|
|
1157
|
+
if (String(pl.surface || "").trim()) (check(pl.surface, ["x", "y"]).params || []).forEach((n) => names.add(n));
|
|
1158
|
+
if (names.size) {
|
|
1159
|
+
if (!pl.params || typeof pl.params !== "object") pl.params = {};
|
|
1160
|
+
const rows = [...names].map((n) => {
|
|
1161
|
+
const cur = typeof pl.params[n] === "number" ? { value: pl.params[n] } : pl.params[n] || {};
|
|
1162
|
+
const cell = (k, ph) => {
|
|
1163
|
+
const input = h("input", { type: "text", inputmode: k === "label" ? "text" : "decimal", class: "form-control", placeholder: ph, "aria-label": `${n}: ${k}`, "data-plot-param": `${n}.${k}` });
|
|
1164
|
+
input.value = cur[k] ?? "";
|
|
1165
|
+
input.addEventListener("input", () => {
|
|
1166
|
+
const raw = input.value.trim();
|
|
1167
|
+
const v = raw === "" ? null : k === "label" ? raw : Number(raw.replace(",", "."));
|
|
1168
|
+
const p = (pl.params[n] = pl.params[n] && typeof pl.params[n] === "object" ? pl.params[n] : {});
|
|
1169
|
+
if (v == null || Number.isNaN(v)) delete p[k]; else p[k] = v;
|
|
1170
|
+
if (!Object.keys(p).length) delete pl.params[n];
|
|
1171
|
+
if (!Object.keys(pl.params).length) delete pl.params;
|
|
1172
|
+
commitSoon();
|
|
1173
|
+
});
|
|
1174
|
+
return input;
|
|
1175
|
+
};
|
|
1176
|
+
return h("tr", {}, h("th", { class: "mono", text: n }), h("td", {}, cell("value", "1")), h("td", {}, cell("min", "-5")), h("td", {}, cell("max", "5")), h("td", {}, cell("label", "nome")));
|
|
1177
|
+
});
|
|
1178
|
+
box.append(fieldWrap({ label: "Controles deslizantes" }, h("table", { class: "sf-plot-params" },
|
|
1179
|
+
h("thead", {}, h("tr", {}, ["", "Valor", "Mín.", "Máx.", "Nome"].map((t) => h("th", { text: t })))), h("tbody", {}, rows))));
|
|
1180
|
+
}
|
|
1181
|
+
// pontos: colar do Excel (x e y), CSV, ou o nome de um arquivo .csv ao lado do deck
|
|
1182
|
+
const pts = h("textarea", { class: "form-control mono", rows: 4, placeholder: "Cole do Excel: coluna x e coluna y", spellcheck: "false", "data-plot-points": "" });
|
|
1183
|
+
pts.value = Array.isArray(pl.points) ? pl.points.map((q) => (Array.isArray(q) ? q : [q.x, q.y]).join("\t")).join("\n") : pl.points ?? "";
|
|
1184
|
+
pts.addEventListener("input", () => { setKey(pl, "points", pts.value.trim() ? pts.value : null); commitSoon(); });
|
|
1185
|
+
const file = h("input", { type: "file", accept: ".csv,.tsv,.txt,text/csv", hidden: true });
|
|
1186
|
+
file.addEventListener("change", async () => { const t = await file.files[0]?.text(); if (t != null) { pts.value = t; setKey(pl, "points", t); commitNow(); } });
|
|
1187
|
+
box.append(fieldWrap({ label: "Pontos medidos (opcional)", hint: "Aparecem como bolinhas junto das curvas. Também aceita o nome de um arquivo ao lado do deck, como dados/medidas.csv." },
|
|
1188
|
+
h("div", { class: "sf-ctl" }, pts, h("button", { class: "btn btn-small", type: "button", onclick: () => file.click() }, icon("upload"), " Importar CSV"), file)));
|
|
1189
|
+
box.append(moreGroup(pl, [f.text("surface", "Superfície 3D: z =", { placeholder: "sin(x)*cos(y)", hint: "Usa x e y no mesmo intervalo; troca as curvas por uma superfície que gira." }),
|
|
1190
|
+
f.pair("y", "Eixo y fixo", ["de", "até"]), f.text("pointsName", "Nome dos pontos"),
|
|
1191
|
+
f.json("data", "Dados Plotly (avançado)", { hint: "Array de traces Plotly: substitui fórmulas e pontos." }), f.json("layout", "Eixos e aparência Plotly (avançado)")], "slide.plot.more"));
|
|
1192
|
+
return box;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// ---- genérico: qualquer campo que o formulário não conhece ----
|
|
1196
|
+
function genericFields(o, used, path, hint) {
|
|
1197
|
+
const keys = Object.keys(o).filter((k) => !used.has(k) && !k.startsWith("_"));
|
|
1198
|
+
if (!keys.length) return null;
|
|
1199
|
+
const box = h("div", {}, hint ? h("div", { class: "sf-hint", text: hint }) : null);
|
|
1200
|
+
keys.forEach((k) => box.append(genericField(o, k)));
|
|
1201
|
+
const det = h("details", { class: "sf-more" }, h("summary", { text: `Outros campos (${keys.length})` }), box);
|
|
1202
|
+
det.open = openGroups.has(path);
|
|
1203
|
+
det.addEventListener("toggle", () => (det.open ? openGroups.add(path) : openGroups.delete(path)));
|
|
1204
|
+
return det;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function genericField(o, k) {
|
|
1208
|
+
const v = o[k];
|
|
1209
|
+
if (typeof v === "boolean") return boolField(o, { k, label: k });
|
|
1210
|
+
if (typeof v === "number") return numberField(o, { k, label: k });
|
|
1211
|
+
if (typeof v === "string") return textField(o, { k, label: k });
|
|
1212
|
+
if (Array.isArray(v) && v.every((x) => typeof x === "string")) return listField(o, { k, label: k, item: T }, "gen");
|
|
1213
|
+
// estruturas: edição como JSON validado
|
|
1214
|
+
const ta = h("textarea", { class: "form-control mono", rows: Math.min(10, JSON.stringify(v, null, 2).split("\n").length) });
|
|
1215
|
+
ta.value = JSON.stringify(v, null, 2);
|
|
1216
|
+
const hint = h("div", { class: "sf-hint" });
|
|
1217
|
+
ta.addEventListener("input", () => {
|
|
1218
|
+
try { o[k] = JSON.parse(ta.value); ta.classList.remove("invalid"); hint.textContent = ""; commitSoon(); }
|
|
1219
|
+
catch (err) { ta.classList.add("invalid"); hint.textContent = "JSON inválido — não aplicado"; }
|
|
1220
|
+
});
|
|
1221
|
+
return h("div", { class: "sf-field" }, h("label", { class: "sf-label", text: k }), ta, hint);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// ---- montagem ----
|
|
1225
|
+
function renderFields(o, fields, path, onAnyChange) {
|
|
1226
|
+
const frag = document.createDocumentFragment();
|
|
1227
|
+
const controls = new Map();
|
|
1228
|
+
for (const spec of fields) {
|
|
1229
|
+
const node = renderField(o, spec, path, controls);
|
|
1230
|
+
if (!node) continue;
|
|
1231
|
+
if (spec.k) controls.set(spec.k, node);
|
|
1232
|
+
if (onAnyChange) { node.addEventListener("input", onAnyChange); node.addEventListener("change", onAnyChange); }
|
|
1233
|
+
frag.append(node);
|
|
1234
|
+
}
|
|
1235
|
+
return frag;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function schemaKeys(fields) {
|
|
1239
|
+
const out = new Set();
|
|
1240
|
+
for (const s of fields) {
|
|
1241
|
+
if (s.type === "more") schemaKeys(s.fields).forEach((k) => out.add(k));
|
|
1242
|
+
else [].concat(s.k || []).forEach((k) => out.add(k));
|
|
1243
|
+
}
|
|
1244
|
+
return out;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function render(container, slide, ctx) {
|
|
1248
|
+
CTX = ctx;
|
|
1249
|
+
container.innerHTML = "";
|
|
1250
|
+
const layout = slide.layout || "blocks";
|
|
1251
|
+
const fields = LAYOUTS[layout] || LAYOUTS.blocks;
|
|
1252
|
+
container.append(renderFields(slide, fields.filter((s) => s.type !== "more"), "slide"));
|
|
1253
|
+
if (["code", "codewalk", "api"].includes(layout)) container.append(renderFields(slide, [f.select("density", "Densidade técnica", [["comfortable","Confortável"],["compact","Compacta"],["dense","Mais conteúdo"]], {empty:false, default:"comfortable"})], "slide.density"));
|
|
1254
|
+
const own = fields.find((s) => s.type === "more")?.fields || [];
|
|
1255
|
+
const ownKeys = schemaKeys(own);
|
|
1256
|
+
container.append(moreGroup(slide, [...own, ...COMMON_MORE.filter((s) => !ownKeys.has(s.k))], "slide.more"));
|
|
1257
|
+
const used = new Set([...schemaKeys(fields), ...schemaKeys(COMMON_MORE), ...SLIDE_RESERVED]);
|
|
1258
|
+
const extra = genericFields(slide, used, "slide.other", "Campos que este layout não usa ou que o editor ainda não conhece. Continuam no YAML.");
|
|
1259
|
+
if (extra) container.append(extra);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
window.SlideForm = { render, LAYOUTS, ELEMENT_KINDS };
|
|
1263
|
+
})();
|