sagadeck 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +9 -2
  2. package/SKILL.md +1 -1
  3. package/bin/sagadeck.js +1 -1
  4. package/docs/ANDAMENTO.md +302 -0
  5. package/docs/REFERENCIA.md +72 -4
  6. package/package.json +4 -4
  7. package/src/ai/context.js +7 -2
  8. package/src/ai/critique.js +8 -1
  9. package/src/ai/deck-ai.js +36 -12
  10. package/src/ai/document-visuals.js +6 -2
  11. package/src/ai/ia-check.js +13 -4
  12. package/src/ai/ia-config.js +4 -2
  13. package/src/ai/llm.js +12 -7
  14. package/src/ai/math-check.js +318 -0
  15. package/src/ai/quality.js +3 -3
  16. package/src/ai/units.js +85 -0
  17. package/src/api-client.js +42 -10
  18. package/src/build.js +1 -0
  19. package/src/code-highlight.js +19 -2
  20. package/src/code-language.js +13 -0
  21. package/src/fs-retry.js +2 -2
  22. package/src/lessons.js +72 -11
  23. package/src/mcp/server.js +1 -1
  24. package/src/package.js +4 -0
  25. package/src/runtime/api-core.js +122 -11
  26. package/src/runtime/api-ui.js +2 -1
  27. package/src/runtime/base.css +3 -0
  28. package/src/runtime/skins/aurora-dev.css +39 -0
  29. package/src/runtime/skins/terminal-pro.css +33 -0
  30. package/src/runtime/solution.js +131 -0
  31. package/src/studio/ai-chat-route.js +69 -7
  32. package/src/studio/conference-routes.js +44 -0
  33. package/src/studio/conference.js +144 -0
  34. package/src/studio/developer-starters.js +46 -0
  35. package/src/studio/generation-report.js +61 -1
  36. package/src/studio/generation-request.js +80 -0
  37. package/src/studio/ia-routes.js +1 -1
  38. package/src/studio/layout-samples.js +1 -1
  39. package/src/studio/library-routes.js +7 -0
  40. package/src/studio/public/about.js +68 -0
  41. package/src/studio/public/ai-settings.js +4 -4
  42. package/src/studio/public/app.js +14 -2
  43. package/src/studio/public/chat.js +11 -3
  44. package/src/studio/public/conference-ui.js +91 -0
  45. package/src/studio/public/developer-authoring.css +12 -0
  46. package/src/studio/public/developer-authoring.js +306 -0
  47. package/src/studio/public/generation-guide.js +42 -0
  48. package/src/studio/public/index.html +21 -0
  49. package/src/studio/public/library.css +36 -0
  50. package/src/studio/public/library.html +4 -1
  51. package/src/studio/public/library.js +118 -28
  52. package/src/studio/public/slide-form.js +17 -17
  53. package/src/studio/public/style.css +25 -0
  54. package/src/studio/public/ui-icons.js +1 -1
  55. package/src/studio/server.js +20 -8
  56. package/src/themes.js +48 -0
@@ -6,6 +6,9 @@ const KEYWORDS = {
6
6
  JavaScript: new Set("async await break case catch class const continue debugger default delete do else export extends finally for from function if import in instanceof let new of return static super switch this throw try typeof var void while with yield".split(" ")),
7
7
  TypeScript: new Set("abstract any as asserts async await bigint boolean break case catch class const constructor continue declare default delete do else enum export extends finally for from function get if implements import in infer instanceof interface is keyof let namespace never new number object of package private protected public readonly return set static string super switch symbol this throw try type typeof undefined unique unknown var void while with yield".split(" ")),
8
8
  "C#": new Set("abstract as async await base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw try typeof uint ulong unchecked unsafe ushort using virtual void volatile while".split(" ")),
9
+ Bash: new Set("if then else elif fi for in do done while until case esac function select time export local readonly return exit source".split(" ")),
10
+ JSON: new Set(),
11
+ SQL: new Set("SELECT FROM WHERE JOIN INNER LEFT RIGHT FULL OUTER CROSS ON AS AND OR NOT IN IS NULL DISTINCT GROUP BY ORDER HAVING LIMIT OFFSET ASC DESC INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE INDEX VIEW DROP ALTER WITH UNION ALL EXISTS CASE WHEN THEN ELSE END BETWEEN LIKE RETURNING BEGIN COMMIT ROLLBACK PRIMARY KEY REFERENCES DEFAULT".split(" ")),
9
12
  };
10
13
 
11
14
  const TYPES = {
@@ -14,11 +17,19 @@ const TYPES = {
14
17
  JavaScript: new Set("Array Boolean Date Map Number Object Promise Set String".split(" ")),
15
18
  TypeScript: new Set("Array any bigint boolean never number object string symbol unknown void".split(" ")),
16
19
  "C#": new Set("bool byte char DateTime decimal double float int List long object short string Task uint ulong var void".split(" ")),
20
+ Bash: new Set(),
21
+ JSON: new Set(),
22
+ SQL: new Set("INT INTEGER BIGINT TEXT VARCHAR BOOLEAN FLOAT DOUBLE DECIMAL NUMERIC DATE TIMESTAMP JSON JSONB".split(" ")),
17
23
  };
18
24
 
25
+ const SHELL_COMMANDS = new Set("curl echo printf git npm node python python3 pip pytest jq cat grep sed awk ls cd pwd mkdir rm cp mv sleep read test find sort head tail".split(" "));
26
+
19
27
  const CONSTANTS = new Set(["False", "None", "True", "false", "null", "true", "undefined", "NaN", "Infinity"]);
20
28
 
21
29
  function tokenPattern(language) {
30
+ if (language === "Bash") return /(?<comment>(?<!\S)#[^\n]*)|(?<string>'[^']*'|"(?:\\[\s\S]|[^"\\])*")|(?<variable>\$(?:\{[^}\n]+\}|[A-Za-z_][\w]*|[\d?@#*!$-]))|(?<flag>(?<!\S)--?[A-Za-z][\w-]*(?:=[^\s]+)?)|(?<number>\b\d+(?:\.\d+)?\b)|(?<identifier>[A-Za-z_][\w]*)|(?<operator>[|&;<>]+)|(?<punctuation>[{}()\[\]])/g;
31
+ if (language === "JSON") return /(?<comment>\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(?<string>"(?:\\.|[^"\\])*")|(?<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(?<identifier>true|false|null)|(?<punctuation>[{}\[\],:])/g;
32
+ if (language === "SQL") return /(?<comment>--[^\n]*|\/\*[\s\S]*?\*\/)|(?<string>'(?:''|[^'])*'|"(?:""|[^"])*"|`[^`]*`)|(?<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(?<identifier>[A-Za-z_][\w$]*)|(?<operator>[-+*/%=&|^!<>~]+)|(?<punctuation>[{}()\[\].,;])/g;
22
33
  const comments = language === "Python"
23
34
  ? "#[^\\n]*"
24
35
  : "\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/";
@@ -32,13 +43,18 @@ function tokenPattern(language) {
32
43
  function classify(match, language, source, offset) {
33
44
  const groups = match.groups;
34
45
  if (groups.comment) return "comment";
46
+ if (groups.variable) return "constant";
47
+ if (groups.flag) return "keyword";
48
+ if (groups.string && language === "JSON" && /^\s*:/.test(source.slice(offset + match[0].length))) return "type";
35
49
  if (groups.string) return "string";
36
50
  if (groups.number) return "number";
37
51
  if (groups.identifier) {
38
52
  const word = groups.identifier;
39
- if (KEYWORDS[language].has(word)) return "keyword";
53
+ const normalized = language === "SQL" ? word.toUpperCase() : word;
54
+ if (KEYWORDS[language].has(normalized)) return "keyword";
40
55
  if (CONSTANTS.has(word)) return "constant";
41
- if (TYPES[language].has(word)) return "type";
56
+ if (TYPES[language].has(normalized)) return "type";
57
+ if (language === "Bash" && SHELL_COMMANDS.has(word)) return "function";
42
58
  if (/^\s*\(/.test(source.slice(offset + match[0].length))) return "function";
43
59
  }
44
60
  if (groups.operator) return "operator";
@@ -47,6 +63,7 @@ function classify(match, language, source, offset) {
47
63
  }
48
64
 
49
65
  export function highlightCode(source, language) {
66
+ if (!KEYWORDS[language]) return source.split("\n").map(esc);
50
67
  const pattern = tokenPattern(language);
51
68
  const lines = [""];
52
69
  const append = (value, kind = "") => {
@@ -11,6 +11,11 @@ const EXTENSIONS = new Map([
11
11
  [".mts", "TypeScript"],
12
12
  [".cts", "TypeScript"],
13
13
  [".cs", "C#"],
14
+ [".sh", "Bash"],
15
+ [".bash", "Bash"],
16
+ [".json", "JSON"],
17
+ [".jsonc", "JSON"],
18
+ [".sql", "SQL"],
14
19
  ]);
15
20
 
16
21
  const ALIASES = new Map([
@@ -26,6 +31,13 @@ const ALIASES = new Map([
26
31
  ["c#", "C#"],
27
32
  ["csharp", "C#"],
28
33
  ["cs", "C#"],
34
+ ["bash", "Bash"],
35
+ ["shell", "Bash"],
36
+ ["sh", "Bash"],
37
+ ["curl", "Bash"],
38
+ ["json", "JSON"],
39
+ ["jsonc", "JSON"],
40
+ ["sql", "SQL"],
29
41
  ]);
30
42
 
31
43
  export function inferCodeLanguage(filename) {
@@ -48,5 +60,6 @@ export function resolveCodeLanguage(language, filename) {
48
60
 
49
61
  export function displayCodeLanguage(language, filename) {
50
62
  const requested = String(language || "").trim();
63
+ if (requested.toLowerCase() === "curl") return "cURL";
51
64
  return normalizeCodeLanguage(requested) || requested || inferCodeLanguage(filename);
52
65
  }
package/src/fs-retry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Renomear tolerante a arquivo travado. A biblioteca mora em Documentos, que no Windows costuma estar no OneDrive:
2
2
  // enquanto sincroniza (e também o antivírus ou o indexador), o arquivo fica preso por um instante e o rename falha
3
3
  // com EPERM/EBUSY/EACCES. Em vez de perder o salvamento da pessoa, tenta de novo com espera curta e crescente
4
- // (~1 s no total). Qualquer outro erro (destino inexistente, disco cheio…) sobe na hora.
4
+ // (~4 s no total: a CI do Windows já segurou o arquivo por mais de 1 s). Qualquer outro erro (destino inexistente, disco cheio…) sobe na hora.
5
5
  //
6
6
  // Copiar e remover pasta NÃO usam fs.cpSync/fs.rmSync recursivos: o código nativo deles (std::filesystem) no
7
7
  // Windows quebra o processo inteiro (0xC0000409, sem exceção para pegar) ou "faz nada" quando o caminho tem
@@ -12,7 +12,7 @@ import fs from "node:fs";
12
12
  import path from "node:path";
13
13
 
14
14
  const LOCKED = new Set(["EPERM", "EBUSY", "EACCES"]);
15
- export const RENAME_DELAYS = [20, 40, 80, 150, 250, 450];
15
+ export const RENAME_DELAYS = [20, 40, 80, 150, 250, 450, 700, 1000, 1300];
16
16
 
17
17
  const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
18
18
 
package/src/lessons.js CHANGED
@@ -8,6 +8,7 @@ import katex from "katex";
8
8
  import { md, esc } from "./markup.js";
9
9
  import "./runtime/formula.js";
10
10
  import "./runtime/calc.js";
11
+ import "./runtime/solution.js";
11
12
 
12
13
  const F = globalThis.SagaFormula;
13
14
  const tex = (s, display = true) => katex.renderToString(String(s ?? ""), { displayMode: display, throwOnError: false, strict: "ignore", trust: false, maxExpand: 1000, maxSize: 20 });
@@ -29,32 +30,92 @@ const panel = (i, inner, cls = "") => `<section class="lesson-panel dyn-frame ${
29
30
  const PROSE = /\p{L}{3,}/u;
30
31
  const isProse = (s) => !/\\[a-zA-Z]|[\^_{}]/.test(s) && PROSE.test(String(s).replace(/\b(sen|cos|tan|tg|log|ln|exp|max|min|lim|mod)\b/g, ""));
31
32
  const mathOrText = (s) => (isProse(String(s ?? "")) ? `<span class="sol-txt t f-body">${md(String(s))}</span>` : tex(s, false));
32
- function givenTex(g) {
33
+ // % (e # & _ $) são comandos no LaTeX: "60 %" saía como erro vermelho na conta da criança
34
+ const unitTex = (u) => String(u).replace(/(?<!\\)[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,");
35
+ const unitSuffix = (u) => (u ? `\\ \\mathrm{${unitTex(u)}}` : "");
36
+ function givenTex(g, value = null) {
33
37
  if (typeof g === "string") return g;
34
38
  if (g.latex) return g.latex;
35
- const value = typeof g.value === "string" && isProse(g.value) ? `\\text{${g.value.replace(/[{}\\$]/g, "")}}` : commaTex(g.value);
36
- // % (e # & _ $) são comandos no LaTeX: "60 %" saía como erro vermelho na conta da criança
37
- const unit = g.unit ? String(g.unit).replace(/(?<!\\)[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,") : "";
38
- return `${g.symbol ?? ""} = ${value}${unit ? `\\ \\mathrm{${unit}}` : ""}`;
39
+ value ??= typeof g.value === "string" && isProse(g.value) ? `\\text{${g.value.replace(/[{}\\$]/g, "")}}` : commaTex(g.value);
40
+ return `${symbolTex(g.symbol)} = ${value}${unitSuffix(g.unit)}`;
39
41
  }
42
+ // "p_atm" é p com índice "atm": sem chaves o LaTeX subscrevia só o "a" (aparecia "pₐtm" num exercício de aula)
43
+ const symbolTex = (s) => String(s ?? "").replace(/_([A-Za-z0-9]{2,})/g, "_{$1}");
44
+ // nome da conta em LaTeX, quando o passo não traz nem latex nem symbol: D2 → D_{2}, rho → \rho, fator → \mathrm{fator}
45
+ const GREEK = /^(alpha|beta|gamma|delta|epsilon|theta|lambda|mu|nu|xi|pi|rho|sigma|tau|phi|chi|psi|omega|Delta|Omega|Phi|Sigma)(\d*)$/;
46
+ const nameTex = (n) => (n.startsWith("_") ? "" : GREEK.test(n) ? n.replace(GREEK, (_, g, d) => `\\${g}${d ? `_{${d}}` : ""}`) : /^[A-Za-z]\d*$/.test(n) ? n.replace(/^(.)(\d+)$/, "$1_{$2}") : `\\mathrm{${n}}`);
47
+ // {{nome}} ou {{expressão:casas}}: o valor calculado no lugar
48
+ const PH = /\{\{\s*([^{}:]+?)\s*(?::\s*(\d+)\s*)?\}\}/g;
49
+ const S = globalThis.SagaSolution;
50
+ // \htmlData só para a marca do número que a apresentação troca ao vivo
51
+ const texLive = (s, display = true) => katex.renderToString(String(s ?? ""), { displayMode: display, throwOnError: false, strict: "ignore", trust: (c) => c.command === "\\htmlData" && Object.keys(c.attributes || {}).every((k) => k === "data-solv"), maxExpand: 1000, maxSize: 20 });
40
52
  export function solutionHTML(s, ctx, head) {
41
53
  const steps = (s.steps || []).map((x) => (typeof x === "string" ? { latex: x } : x || {}));
42
54
  const givens = [].concat(s.givens || s.data || []);
43
55
  const hasAnswer = !!s.answer;
56
+ const answer = hasAnswer ? (typeof s.answer === "string" ? { latex: s.answer } : s.answer) : null;
57
+ // contas: dados com símbolo e valor viram variáveis; passo (e resposta) com fn é calculado pelo motor
58
+ const m = S.model({ ...s, steps, answer });
59
+ const { env, results } = S.evaluate(m);
60
+ const live = !!s.interactive && m.vars.length > 0;
61
+ const T = live ? texLive : tex;
62
+ const marks = [], markAt = new Map();
63
+ const decOf = (name) => m.vars.find((v) => v.name === name)?.decimals ?? m.calcs.find((c) => c.name === name)?.decimals;
64
+ const mark = (expr, d) => {
65
+ const decimals = d != null && d !== "" ? Number(d) : decOf(expr), key = `${expr}|${decimals ?? ""}`;
66
+ if (!markAt.has(key)) { markAt.set(key, marks.length); marks.push({ expr, ...(decimals != null ? { decimals } : {}) }); }
67
+ return { k: markAt.get(key), decimals, value: S.evalExpr(expr, env) };
68
+ };
69
+ const numTex = (r) => { const t = S.format(r.value, r.decimals, "tex"); return live ? `\\htmlData{solv=${r.k}}{${t}}` : t; };
70
+ const fillTex = (str) => String(str ?? "").replace(PH, (_, e, d) => numTex(mark(e, d)));
71
+ const fillText = (str) => {
72
+ const nums = [];
73
+ const out = md(String(str ?? "").replace(PH, (_, e, d) => {
74
+ const r = mark(e, d), t = esc(S.format(r.value, r.decimals, "text"));
75
+ nums.push(live ? `<span data-solv="${r.k}" data-solv-mode="text">${t}</span>` : t);
76
+ return `\uE000${nums.length - 1}\uE001`;
77
+ }));
78
+ return out.replace(/\uE000(\d+)\uE001/g, (_, i) => nums[i]);
79
+ };
80
+ const errors = m.calcs.filter((c) => results[c.key].error).map((c) => `${c.label}: ${results[c.key].error}`);
81
+ if (errors.length) ctx?.warnings?.push(...errors.map((e) => `exercício resolvido: ${e}`));
82
+ // LaTeX do passo calculado: o valor onde a pessoa pôs {{nome}} (o nome do próprio passo), ou "≈ valor unidade" no fim
83
+ const calcTex = (o, key) => {
84
+ const c = m.calcs.find((x) => x.key === key);
85
+ if (!c) return o.latex ? fillTex(o.latex) : "";
86
+ if ([...String(o.latex ?? "").matchAll(PH)].some((x) => x[1] === c.name)) return fillTex(o.latex);
87
+ const r = mark(c.name, c.decimals);
88
+ const exact = r.value != null && S.parseNum(S.format(r.value, c.decimals, "text")) === r.value;
89
+ const lhs = o.latex ? fillTex(o.latex) : o.symbol ? String(o.symbol) : nameTex(c.name);
90
+ const rel = !lhs || /(=|\\approx|≈)\s*$/.test(lhs) ? "" : exact ? "= " : "\\approx ";
91
+ return `${lhs}${lhs ? " " : ""}${rel}${numTex(r)}${unitSuffix(o.unit)}`;
92
+ };
93
+ const calcError = (key) => { const c = m.calcs.find((x) => x.key === key); return c && results[key].error ? `<div class="sol-error dyn-error f-body">${esc(`${c.label[0].toUpperCase()}${c.label.slice(1)}: ${results[key].error}`)}</div>` : ""; };
94
+ const varOf = (g) => (g && typeof g === "object" ? m.vars.find((v) => v.name === (g.name ? String(g.name) : S.varName(g.symbol))) : null);
95
+ const givenHTML = (g) => {
96
+ if (typeof g === "string") return `<div class="sol-given"><div class="sol-tex">${mathOrText(g)}</div></div>`;
97
+ const v = live && !g.latex ? varOf(g) : null;
98
+ const t = v ? givenTex(g, numTex(mark(v.name, v.decimals))) : g.latex ? fillTex(g.latex) : givenTex(g);
99
+ const slider = v ? `<input type="range" class="sol-slider" data-solv-in="${esc(v.name)}" min="${v.min}" max="${v.max}" step="${v.step}" value="${v.value}" aria-label="${esc(v.label)}">` : "";
100
+ return `<div class="sol-given${slider ? " live" : ""}"><div class="sol-tex">${T(t, false)}</div>${g.label ? `<span class="t f-body">${md(g.label)}</span>` : ""}${slider}</div>`;
101
+ };
44
102
  const n = 1 + steps.length + (hasAnswer ? 1 : 0); // enunciado · cada passo · resposta
45
- const left = `<div class="sol-problem">${s.problem ? `<div class="sol-text t f-body">${md(s.problem)}</div>` : ""}
46
- ${givens.length ? `<div class="sol-givens"><div class="sol-label t f-label">Dados</div>${givens.map((g) => `<div class="sol-given"><div class="sol-tex">${typeof g === "string" ? mathOrText(g) : tex(givenTex(g), false)}</div>${g.label ? `<span class="t f-body">${md(g.label)}</span>` : ""}</div>`).join("")}</div>` : ""}
103
+ const reset = live ? `<button type="button" class="sol-reset" data-solv-reset>Restaurar</button>` : "";
104
+ const left = `<div class="sol-problem">${s.problem ? `<div class="sol-text t f-body">${fillText(s.problem)}</div>` : ""}
105
+ ${givens.length ? `<div class="sol-givens"><div class="sol-label t f-label">Dados${reset}</div>${givens.map(givenHTML).join("")}</div>` : ""}
47
106
  ${s.find ? `<div class="sol-find"><span class="sol-label t f-label">Pede-se</span><div class="sol-tex">${mathOrText(s.find)}</div></div>` : ""}</div>`;
48
- const stepHTML = (st, i, k) => `<article class="sol-step${i === k - 1 ? " now" : ""}"><span class="sol-n t f-label">${String(i + 1).padStart(2, "0")}</span><div class="sol-body">${st.text ? `<div class="sol-why t f-body">${md(st.text)}</div>` : ""}${st.latex ? `<div class="sol-math">${tex(st.latex)}</div>` : ""}${st.note ? `<div class="sol-note t f-body">${md(st.note)}</div>` : ""}</div></article>`;
49
- const answer = hasAnswer ? (typeof s.answer === "string" ? { latex: s.answer } : s.answer) : null;
107
+ const stepTex = steps.map((st, i) => calcTex(st, `p${i}`));
108
+ const stepHTML = (st, i, k) => `<article class="sol-step${i === k - 1 ? " now" : ""}"><span class="sol-n t f-label">${String(i + 1).padStart(2, "0")}</span><div class="sol-body">${st.text ? `<div class="sol-why t f-body">${fillText(st.text)}</div>` : ""}${stepTex[i] ? `<div class="sol-math">${T(stepTex[i])}</div>` : ""}${calcError(`p${i}`)}${st.note ? `<div class="sol-note t f-body">${fillText(st.note)}</div>` : ""}</div></article>`;
109
+ const answerTex = answer ? calcTex(answer, "resp") : "";
50
110
  const frames = Array.from({ length: n }, (_, k) => {
51
111
  const done = Math.min(k, steps.length);
52
112
  const list = steps.slice(0, done).map((st, i) => stepHTML(st, i, done)).join("");
53
- const final = hasAnswer && k === n - 1 ? `<div class="sol-answer"><span class="sol-label t f-label">${esc(answer.label || "Resposta")}</span>${answer.latex ? `<div class="sol-math">${tex(answer.latex)}</div>` : ""}${answer.text ? `<div class="t f-body">${md(answer.text)}</div>` : ""}</div>` : "";
113
+ const final = hasAnswer && k === n - 1 ? `<div class="sol-answer"><span class="sol-label t f-label">${esc(answer.label || "Resposta")}</span>${answerTex ? `<div class="sol-math">${T(answerTex)}</div>` : ""}${calcError("resp")}${answer.text ? `<div class="t f-body">${fillText(answer.text)}</div>` : ""}</div>` : "";
54
114
  const empty = k === 0 ? `<div class="sol-think t f-display">${md(s.prompt || "Por onde você começaria?")}</div>` : "";
55
115
  return panel(k, `<div class="sol-steps">${empty}${list}${final}</div>`);
56
116
  }).join("");
57
- return `<div class="L-solution dyn" data-lesson="solution" data-lesson-count="${n}">${head(s)}<div class="sol-row">${left}<div class="dyn-frames">${frames}</div></div></div>`;
117
+ const payload = live ? `<script type="application/json" class="sol-model">${JSON.stringify({ ...m, marks }).replace(/</g, "\\u003c")}</script>` : "";
118
+ return `<div class="L-solution dyn" data-lesson="solution" data-lesson-count="${n}"${live ? " data-solution" : ""}>${head(s)}<div class="sol-row">${left}<div class="dyn-frames">${frames}</div></div>${payload}</div>`;
58
119
  }
59
120
 
60
121
  // ------------------------------------------------------------------------------------------------ calc
package/src/mcp/server.js CHANGED
@@ -135,7 +135,7 @@ const TOOLS = [
135
135
  inputSchema: {
136
136
  type: "object",
137
137
  properties: {
138
- so: { type: "array", items: { type: "string", enum: ["texto", "visao", "imagem", "pesquisa", "video"] }, description: "Só estes recursos (padrão: todos)" },
138
+ so: { type: "array", items: { type: "string", enum: ["texto", "visao", "imagem", "pesquisa", "video", "conferencia"] }, description: "Só estes recursos (padrão: todos)" },
139
139
  rapido: { type: "boolean", description: "Pula geração de imagem e vídeo (os lentos e cobrados)" },
140
140
  video_real: { type: "boolean", description: "Gera um clipe curto de verdade e confere o MP4 (cobrado; leva minutos)" },
141
141
  },
package/src/package.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // <nome>.yaml a apresentação (sem campos internos da máquina de quem salvou)
6
6
  // imagens/, widgets/… os arquivos, nos mesmos caminhos relativos do YAML
7
7
  // assets/ arquivos que estavam FORA da pasta do deck (o YAML do pacote aponta para cá)
8
+ // .sagadeck/geracao.json o pedido que originou a apresentação com IA, quando existe
8
9
  // FALTANDO.txt o que o deck usa mas não existia ao salvar
9
10
  //
10
11
  // Também abre um .zip comum com um .yaml dentro (versão sem manifesto).
@@ -93,6 +94,9 @@ export async function packDeck(spec, { baseDir, name = "apresentacao", generator
93
94
  if (size <= budget) { root.file(q.rel, fs.readFileSync(q.abs)); files.push(q.rel); budget -= size; }
94
95
  else skipped.push(q.rel);
95
96
  }
97
+ // o pedido que originou a apresentação (src/studio/generation-request.js) viaja junto, para auditar depois
98
+ const request = path.join(dir, ".sagadeck", "geracao.json");
99
+ if (fs.existsSync(request)) { root.file(".sagadeck/geracao.json", fs.readFileSync(request)); files.push(".sagadeck/geracao.json"); }
96
100
  for (const a of assets) {
97
101
  if (a.exists) { root.file(a.rel, fs.readFileSync(a.abs)); files.push(a.rel); }
98
102
  else missing.push(a.ref);
@@ -35,11 +35,45 @@
35
35
 
36
36
  // {{nome}} em qualquer texto do pedido (URL, cabeçalhos, corpo). Sem valor: fica como está.
37
37
  const VAR = /\{\{\s*([\w.-]+)\s*\}\}/g;
38
+ function renderBody(body, vars, encoding) {
39
+ if (typeof body !== "string" || !["json", "urlencoded"].includes(encoding)) return render(body, vars);
40
+ if (encoding === "urlencoded") return body.replace(VAR, (all, key) => vars && vars[key] != null ? encodeURIComponent(String(vars[key])) : all);
41
+ // Interpola no texto original, sem converter números ou reformatar o JSON. Dentro de uma
42
+ // string escapa só o valor; fora dela, insere um literal JSON. Marcador sem valor continua.
43
+ let result = "", quoted = false;
44
+ for (let at = 0; at < body.length; at++) {
45
+ if (body.startsWith("{{", at)) {
46
+ const marker = body.slice(at).match(/^\{\{\s*([\w.-]+)\s*\}\}/);
47
+ if (marker && vars && vars[marker[1]] != null) {
48
+ const value = vars[marker[1]];
49
+ result += quoted ? JSON.stringify(String(value)).slice(1, -1) : JSON.stringify(value);
50
+ at += marker[0].length - 1; continue;
51
+ }
52
+ }
53
+ const char = body[at]; result += char;
54
+ if (quoted && char === "\\" && at + 1 < body.length) { result += body[++at]; continue; }
55
+ if (char === '"') quoted = !quoted;
56
+ }
57
+ return result;
58
+ }
59
+ function renderURL(url, vars, encoding) {
60
+ if (encoding !== "query") return render(url, vars);
61
+ const text = String(url), query = text.indexOf("?"), fragment = text.indexOf("#");
62
+ return text.replace(VAR, (all, key, at) => {
63
+ if (!vars || vars[key] == null) return all;
64
+ const value = String(vars[key]);
65
+ return query >= 0 && at > query && (fragment < 0 || at < fragment) ? encodeURIComponent(value) : value;
66
+ });
67
+ }
38
68
  function render(value, vars) {
39
69
  if (typeof value === "string") return value.replace(VAR, (all, k) => (vars && vars[k] != null ? String(vars[k]) : all));
40
70
  if (Array.isArray(value)) return value.map((x) => render(x, vars));
41
71
  if (value && typeof value === "object" && value.__py) return value;
42
- if (value && typeof value === "object") { const o = {}; for (const [k, v] of Object.entries(value)) o[render(k, vars)] = render(v, vars); return o; }
72
+ if (value && typeof value === "object") {
73
+ const o = {};
74
+ for (const [k, v] of Object.entries(value)) o[render(k, vars)] = k === "body" && value.bodyEncoding ? renderBody(v, vars, value.bodyEncoding) : k === "url" && value.urlEncoding ? renderURL(v, vars, value.urlEncoding) : render(v, vars);
75
+ return o;
76
+ }
43
77
  return value;
44
78
  }
45
79
  function missing(value, vars) {
@@ -82,6 +116,8 @@
82
116
  url: String(req.url || ""),
83
117
  headers: req.headers && typeof req.headers === "object" ? req.headers : {},
84
118
  body: req.body,
119
+ ...(["json", "urlencoded"].includes(req.bodyEncoding) ? { bodyEncoding: req.bodyEncoding } : {}),
120
+ ...(req.urlEncoding === "query" ? { urlEncoding: "query" } : {}),
85
121
  form: req.form && typeof req.form === "object" ? req.form : null, // multipart: { campo: "@file" | texto }
86
122
  auth: req.auth !== false,
87
123
  },
@@ -202,12 +238,12 @@
202
238
  }
203
239
 
204
240
  function curlCmd(r, tokenVar, extra, fileName) {
205
- const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${dq(r.url)}`];
241
+ const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${r._urlShell || dq(r.url)}`];
206
242
  if (r.auth) parts.push(`-H "Authorization: Bearer $${tokenVar}"`);
207
- for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${dq(k + ": " + v)}`);
243
+ for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${r._headerShell?.[k] || dq(k + ": " + v)}`);
208
244
  if (r.form) for (const [k, v] of Object.entries(r.form)) parts.push(`-F ${dq(k + "=" + (v === "@file" ? "@" + (fileName || "arquivo") : v))}`);
209
245
  else if (isJsonBody(r.body)) { parts.push(`-H "Content-Type: application/json"`); parts.push(`-d ${shq(JSON.stringify(r.body, null, 2))}`); }
210
- else if (r.body != null) parts.push(`-d ${shq(r.body)}`);
246
+ else if (r.body != null) parts.push(r._stdinBody ? "--data-binary @-" : `-d ${r._bodyShell || shq(r.body)}`);
211
247
  return parts.join(" \\\n ");
212
248
  }
213
249
 
@@ -217,7 +253,70 @@
217
253
  const secretsIn = (v) => { const out = new Set(); JSON.stringify(v || "").replace(/\{\{\s*secret\.([\w-]+)\s*\}\}/g, (_, k) => out.add(k)); return [...out]; };
218
254
  const fileName = (a, vars) => (vars && vars["file.name"]) || (a.file ? String(a.file).split(/[\\/]/).pop() : "arquivo");
219
255
  const usesB64 = (a) => JSON.stringify(a.request.body || "").includes("{{file.base64}}");
256
+ // Para cURL importado, expressões são montadas por contexto. As partes literais continuam
257
+ // literais no shell, e JSON/form/query escapam o segredo só na hora de rodar o exemplo.
258
+ function secretParts(text, encoding) {
259
+ text = String(text); const parts = []; let last = 0, scan = 0, quoted = false;
260
+ const query = text.indexOf("?"), fragment = text.indexOf("#");
261
+ const pattern = /\{\{\s*secret\.([\w-]+)\s*\}\}/g; let match;
262
+ while ((match = pattern.exec(text))) {
263
+ if (encoding === "json") for (; scan < match.index; scan++) { if (quoted && text[scan] === "\\") scan++; else if (text[scan] === '"') quoted = !quoted; }
264
+ if (match.index > last) parts.push(text.slice(last, match.index));
265
+ const context = encoding === "json" ? (quoted ? "json-string" : "json-literal") : encoding === "urlencoded" || (encoding === "query" && query >= 0 && match.index > query && (fragment < 0 || match.index < fragment)) ? "urlencoded" : "raw";
266
+ parts.push({ name: secretEnv(match[1]), context }); last = pattern.lastIndex; scan = last;
267
+ }
268
+ if (last < text.length || !parts.length) parts.push(text.slice(last));
269
+ return parts;
270
+ }
271
+ function secretExpression(text, encoding, language) {
272
+ return secretParts(text, encoding).map(part => {
273
+ if (typeof part === "string") return JSON.stringify(part);
274
+ const value = language === "python" ? `os.environ[${JSON.stringify(part.name)}]` : `segredo(${JSON.stringify(part.name)})`;
275
+ if (part.context === "json-string") return language === "python" ? `json.dumps(${value}, ensure_ascii=False)[1:-1]` : `JSON.stringify(${value}).slice(1, -1)`;
276
+ if (part.context === "json-literal") return language === "python" ? `json.dumps(${value}, ensure_ascii=False)` : `JSON.stringify(${value})`;
277
+ if (part.context === "urlencoded") return language === "python" ? `quote(${value}, safe="-_.!~*'()")` : `encodeURIComponent(${value})`;
278
+ return value;
279
+ }).join(" + ");
280
+ }
281
+ function curlImported(a, vars) {
282
+ const L = Lines(false), name = fileName(a, vars), helpers = [], helperNames = new Map();
283
+ const cleanVars = Object.fromEntries(Object.entries(vars || {}).filter(([key]) => !key.startsWith("secret.")));
284
+ const shell = (text, encoding) => secretParts(text, encoding).map(part => {
285
+ if (typeof part === "string") return shq(part);
286
+ if (part.context === "raw") return `"$${part.name}"`;
287
+ const key = `${part.name}:${part.context}`;
288
+ let helper = helperNames.get(key);
289
+ if (!helper) {
290
+ helper = `SAGA_SECRET_${helpers.length + 1}`; helperNames.set(key, helper);
291
+ const expression = part.context === "urlencoded" ? "encodeURIComponent(valor)" : part.context === "json-string" ? "JSON.stringify(valor).slice(1,-1)" : "JSON.stringify(valor)";
292
+ const script = `const valor=process.env[${JSON.stringify(part.name)}];if(valor==null)throw new Error(${JSON.stringify("Defina " + part.name)});process.stdout.write(${expression});`;
293
+ helpers.push(`${helper}=$(node -e ${shq(script)})`);
294
+ }
295
+ return `"$${helper}"`;
296
+ }).join("");
297
+ const prepare = original => {
298
+ const r = render(original, { ...cleanVars, "file.name": name });
299
+ r._urlShell = shell(r.url, r.urlEncoding);
300
+ r._headerShell = Object.fromEntries(Object.entries(r.headers || {}).map(([key, value]) => [key, shell(`${key}: ${value}`, "raw")]));
301
+ if (r.body != null) {
302
+ if (typeof r.body === "object") r.body = JSON.stringify(r.body, null, 2);
303
+ r._bodyShell = shell(r.body, r.bodyEncoding);
304
+ r._stdinBody = true; // stdin preserva UTF-8 também com curl.exe no Windows
305
+ }
306
+ return r;
307
+ };
308
+ const req = prepare(a.request), check = a.polling ? prepare(render(a.polling.check, { ...cleanVars, id: "$ID" })) : null;
309
+ if (helpers.length) { L.add("# Bash + Node.js: segredos são escapados sem alterar o JSON ou a query."); helpers.forEach(line => L.add(line)); L.blank(); }
310
+ const command = (r, extra = "") => (r._stdinBody ? `printf '%s' ${r._bodyShell} | \\\n` : "") + curlCmd(r, a.tokenVar, extra, name);
311
+ if (a.mode === "polling") {
312
+ L.add("# 1. inicia", "start").add(command(req), "start");
313
+ L.add(`# guarde ${a.polling.id} da resposta em ID`).blank();
314
+ L.add("# 2. consulta o andamento", "poll").add(command(check), "poll");
315
+ } else L.add(command(req, a.mode === "stream" ? "N" : "") + (a.audio ? ` \\\n --output ${shq(a.audio)}` : ""), "start");
316
+ return L.out();
317
+ }
220
318
  function curl(a, vars) {
319
+ if (a.request.bodyEncoding || a.request.urlEncoding) return curlImported(a, vars);
221
320
  const L = Lines(false);
222
321
  const name = fileName(a, vars);
223
322
  const sec = Object.fromEntries(secretsIn(a.request).map((k) => ["secret." + k, MARK(k)]));
@@ -242,14 +341,14 @@
242
341
 
243
342
  function pyCall(name, r, extra, ind) {
244
343
  ind = ind || "";
245
- const args = [`${dq(r.url)}`];
344
+ const args = [r._urlExpr || dq(r.url)];
246
345
  if (r.auth || Object.keys(r.headers || {}).length) args.push("headers=HEADERS");
247
346
  if (r.form) {
248
347
  const files = Object.entries(r.form).filter(([, v]) => v === "@file"), data = Object.entries(r.form).filter(([, v]) => v !== "@file");
249
348
  if (files.length) args.push(`files={${files.map(([k]) => `${JSON.stringify(k)}: open(${dq(r._fileName || "arquivo")}, "rb")`).join(", ")}}`);
250
349
  if (data.length) args.push(`data={${data.map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(String(v))}`).join(", ")}}`);
251
350
  } else if (isJsonBody(r.body)) args.push(`json=${pyLiteral(r.body, ind + " ")}`);
252
- else if (r.body != null) args.push(`data=${JSON.stringify(String(r.body))}`);
351
+ else if (r.body != null) args.push(`data=${r.bodyEncoding ? `(${r._bodyExpr || JSON.stringify(String(r.body))}).encode("utf-8")` : r._bodyExpr || JSON.stringify(String(r.body))}`);
253
352
  args.push(...(extra || []));
254
353
  return `${name} = requests.${r.method.toLowerCase()}(\n` + args.map((x) => `${ind} ${x},`).join("\n") + `\n${ind})`;
255
354
  }
@@ -260,7 +359,7 @@
260
359
  Object.assign(h, r.headers || {});
261
360
  if (!Object.keys(h).length) return;
262
361
  const val = (v) => { const m = String(v).match(/^\{\{\s*secret\.([\w-]+)\s*\}\}$/); return m ? `os.environ[${JSON.stringify(String(m[1]).toUpperCase().replace(/[^A-Z0-9_]/g, "_"))}]` : JSON.stringify(String(v)); };
263
- const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : val(v)},`).join("\n");
362
+ const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : r.urlEncoding || r.bodyEncoding ? secretExpression(v, "raw", "python") : val(v)},`).join("\n");
264
363
  L.add(`HEADERS = {\n${body}\n}`, null, "cabeçalhos enviados em toda chamada");
265
364
  }
266
365
 
@@ -271,6 +370,12 @@
271
370
  const toPy = (v) => (v === "{{file.base64}}" ? { __py: "CONTEUDO" } : typeof v === "string" && /^\{\{\s*secret\.([\w-]+)\s*\}\}$/.test(v) ? { __py: `os.environ[${JSON.stringify(secretEnv(v.match(/secret\.([\w-]+)/)[1]))}]` } : Array.isArray(v) ? v.map(toPy) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toPy(x)])) : v);
272
371
  const req = { ...render({ ...a.request, body: toPy(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
273
372
  const imports = ["import os"];
373
+ if (req.urlEncoding || req.bodyEncoding) {
374
+ req._urlExpr = secretExpression(req.url, req.urlEncoding, "python");
375
+ if (typeof req.body === "string") req._bodyExpr = secretExpression(req.body, req.bodyEncoding, "python");
376
+ if (req.bodyEncoding === "json") imports.push("import json");
377
+ if (req.urlEncoding || req.bodyEncoding === "urlencoded") imports.push("from urllib.parse import quote");
378
+ }
274
379
  if (b64) imports.push("import base64");
275
380
  if (a.mode === "polling") imports.push("import time");
276
381
  if (a.mode === "stream") imports.push("import json");
@@ -368,9 +473,9 @@
368
473
  opts.push(`body: JSON.stringify(${jsLiteral(r.body, " ")})`);
369
474
  } else {
370
475
  if (hasHeaders) opts.push("headers: HEADERS");
371
- if (r.body != null) opts.push(`body: ${JSON.stringify(String(r.body))}`);
476
+ if (r.body != null) opts.push(`body: ${r._bodyExpr || JSON.stringify(String(r.body))}`);
372
477
  }
373
- const target = url || dq(r.url);
478
+ const target = url || r._urlExpr || dq(r.url);
374
479
  L.add(opts.length ? `const ${name} = await fetch(${target}, {\n${opts.map((o) => " " + o + ",").join("\n")}\n});` : `const ${name} = await fetch(${target});`, tag, why);
375
480
  L.add(`if (!${name}.ok) throw new Error(\`HTTP \${${name}.status}: \${await ${name}.text()}\`);`, tag, "para aqui se deu erro (4xx/5xx)");
376
481
  }
@@ -380,15 +485,21 @@
380
485
  const b64 = usesB64(a);
381
486
  const toJs = (v) => (v === "{{file.base64}}" ? { __js: "CONTEUDO" } : jsSecret(v) || (Array.isArray(v) ? v.map(toJs) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toJs(x)])) : v));
382
487
  const req = { ...render({ ...a.request, body: toJs(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
488
+ const imported = !!(req.urlEncoding || req.bodyEncoding);
489
+ if (imported) {
490
+ req._urlExpr = secretExpression(req.url, req.urlEncoding, "javascript");
491
+ if (typeof req.body === "string") req._bodyExpr = secretExpression(req.body, req.bodyEncoding, "javascript");
492
+ }
383
493
  const fsUse = [b64 || req.form ? "readFile" : "", a.audio ? "writeFile" : ""].filter(Boolean);
384
494
  L.add("// Node.js 18+ · salve como exemplo.mjs e rode: node exemplo.mjs");
385
495
  if (fsUse.length) L.add(`import { ${fsUse.join(", ")} } from "node:fs/promises";`);
386
496
  L.blank();
497
+ if (imported && secretsIn(a.request).length) L.add('const segredo = (nome) => { const valor = process.env[nome]; if (valor == null) throw new Error(`Defina ${nome}`); return valor; };').blank();
387
498
  const auth = req.auth || !!(a.polling && a.polling.check.auth);
388
499
  if (auth) L.add(`const TOKEN = process.env.${a.tokenVar};`, null, "o token de acesso vem de uma variável de ambiente, nunca escrito no código").blank();
389
500
  const h = [];
390
501
  if (auth) h.push(" Authorization: `Bearer ${TOKEN}`,");
391
- for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
502
+ for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${imported ? secretExpression(v, "raw", "javascript") : jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
392
503
  if (h.length) L.add(`const HEADERS = {\n${h.join("\n")}\n};`, null, "cabeçalhos enviados em toda chamada").blank();
393
504
  if (b64) L.add(`const CONTEUDO = (await readFile(${dq(name)})).toString("base64");`, "start", "o arquivo, em base64, vai dentro do JSON").blank();
394
505
  const out = (expr, why) => {
@@ -525,6 +636,6 @@
525
636
 
526
637
  // o slide mexe com arquivo? (upload @file ou {{file.…}})
527
638
  const usesFile = (s) => { const a = s && s._normalized ? s : normalize(s); return !!a.file || a.mic || !!(a.request.form && Object.values(a.request.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(a.request)); };
528
- const api = { isType, cosine, usesFile, parsePath, get, set, render, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
639
+ const api = { isType, cosine, usesFile, parsePath, get, set, render, renderBody, renderURL, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
529
640
  g.SagadeckApiCore = api;
530
641
  })(typeof window !== "undefined" ? window : globalThis);
@@ -61,7 +61,8 @@
61
61
  const bodyTxt = bodyEl ? bodyEl.value : "";
62
62
  let body;
63
63
  if (bodyTxt.trim() !== "" && !cfg.request.form) { // upload: a aba Corpo só mostra os campos do formulário
64
- try { body = JSON.parse(bodyTxt); }
64
+ if (cfg.request.bodyEncoding && typeof cfg.request.body === "string") body = bodyTxt;
65
+ else try { body = JSON.parse(bodyTxt); }
65
66
  catch (e) {
66
67
  if (typeof cfg.request.body === "string") body = bodyTxt;
67
68
  else throw new Error("O corpo não é um JSON válido: " + e.message);
@@ -1303,6 +1303,9 @@ html[data-motion="subtle"] .lesson-panel.active{animation:lesson-soft .6s cubic-
1303
1303
  .sol-label{font-size:20px;color:var(--em);letter-spacing:.08em;white-space:nowrap}
1304
1304
  .sol-givens{display:flex;flex-direction:column;gap:10px}
1305
1305
  .sol-given{display:flex;align-items:baseline;gap:16px;font-size:28px}.sol-given .sol-tex{min-width:7em}.sol-given>span{font-size:22px;color:var(--muted)}
1306
+ .sol-given.live{flex-wrap:wrap;row-gap:4px}.sol-slider{flex:1 0 100%;height:30px;accent-color:var(--hi);cursor:pointer}
1307
+ .sol-reset{font:inherit;font-size:16px;letter-spacing:normal;color:var(--fg);background:transparent;border:1px solid var(--line);border-radius:8px;padding:4px 12px;margin-left:16px;cursor:pointer}.sol-reset:hover{border-color:var(--fg)}
1308
+ .sol-error{font-size:20px;margin-top:4px;padding:8px 14px;border-radius:12px}
1306
1309
  .sol-find{display:flex;align-items:baseline;gap:16px;font-size:30px;padding-top:14px;border-top:1px dashed var(--line)}
1307
1310
  .sol-steps{flex:1;display:flex;flex-direction:column;justify-content:flex-end;gap:10px;min-height:0;overflow:hidden;-webkit-mask:linear-gradient(to bottom,transparent 0,#000 60px);mask:linear-gradient(to bottom,transparent 0,#000 60px)}.sol-steps.sol-fit{-webkit-mask:none;mask:none}
1308
1311
  /* passos anteriores em tamanho menor só quando não cabem todos no normal (com 3 passos curtos, ficavam miúdos à toa) */
@@ -0,0 +1,39 @@
1
+ /* Aurora Dev: luzes nos cantos, linha orbital e painéis de leitura opacos. A rede ambiente do runtime respeita
2
+ movimento reduzido, motion: none, troca de slide e exportação sem depender de animação CSS contínua. */
3
+ .th-aurora-dev .orn i:nth-child(1){display:block;right:-220px;top:-300px;width:920px;height:820px;border-radius:50%;background:radial-gradient(ellipse,color-mix(in srgb,var(--hi) 18%,transparent),transparent 66%)}
4
+ .th-aurora-dev .orn i:nth-child(2){display:block;left:-240px;bottom:-340px;width:860px;height:740px;border-radius:50%;background:radial-gradient(ellipse,color-mix(in srgb,var(--em) 12%,transparent),transparent 66%)}
5
+ .th-aurora-dev .ambient{opacity:.4!important;mask-image:linear-gradient(90deg,transparent 15%,#000)}
6
+ .th-aurora-dev .kicker{align-self:flex-start;color:var(--em);border-top:3px double var(--em);padding:14px 0 0;font-size:20px;letter-spacing:.16em}
7
+ .th-aurora-dev .L-cover:not(:has(.cv-fig)) .cv-main,.th-aurora-dev .L-end:not(:has(.en-fig)) .en-main{max-width:1390px;margin:0 auto;width:100%;gap:34px}
8
+ .th-aurora-dev :is(.L-cover,.L-end) .ttl{padding-bottom:32px;background:linear-gradient(90deg,var(--hi),var(--em),transparent) no-repeat left bottom/460px 3px}
9
+ .th-aurora-dev .L-cover .sub{max-width:1200px}
10
+ .th-aurora-dev .L-section{flex-direction:column;justify-content:center;align-items:flex-start;gap:28px}
11
+ .th-aurora-dev .L-section .sc-num{font-family:'IBM Plex Mono',monospace;font-size:72px;line-height:1.2;padding:12px 30px;background:var(--surface);border:1px solid var(--hi);border-radius:100px;color:var(--hi)}
12
+ .th-aurora-dev .L-section .sc-text{flex:none;width:100%;gap:24px}
13
+ .th-aurora-dev .L-section:has(.sc-fig){display:grid;grid-template-columns:1fr 500px;align-content:center;column-gap:50px}
14
+ .th-aurora-dev .L-section .sc-fig{grid-column:2;grid-row:1/span 2;width:500px;height:580px}
15
+ .th-aurora-dev .L-section .sc-num{justify-self:start}
16
+ .th-aurora-dev .hd{padding-bottom:22px;border-bottom:2px solid color-mix(in srgb,var(--hi) 40%,var(--line));gap:18px}
17
+ .th-aurora-dev :is(.card:not(.card-hi),.stat-card,.step-card){background:var(--surface);border:1px solid var(--line);border-radius:24px;box-shadow:inset 0 2px 0 color-mix(in srgb,var(--hi) 22%,transparent),0 16px 45px #0002}
18
+ .th-aurora-dev .cd-ico{border-radius:16px;background:color-mix(in srgb,var(--hi) 13%,var(--surface));color:var(--em)}
19
+ .th-aurora-dev .li-n{color:var(--em);border-bottom:2px solid var(--hi);padding-bottom:8px;font-family:'IBM Plex Mono',monospace}
20
+ .th-aurora-dev .li-b{width:14px;height:14px;background:var(--hi);border:0;border-radius:50%;box-shadow:0 0 0 6px color-mix(in srgb,var(--hi) 15%,transparent)}
21
+ .th-aurora-dev .L-quote{padding:36px 50px;border:1px solid var(--line);background:var(--surface);border-radius:24px}
22
+ .th-aurora-dev .q-mark{color:var(--em)}
23
+ .th-aurora-dev .st-body{padding-left:32px;border-left:3px solid var(--em)}
24
+ .th-aurora-dev .nb-val,.th-aurora-dev .st-val{color:var(--hi);font-family:'Space Grotesk',sans-serif}
25
+ /* É a superfície que separa a sintaxe das luzes; não use transparência no código. */
26
+ .th-aurora-dev :is(.code,.codewalk-editor,.api-req,.api-res){background:#131A30;color:#F1F2FF;border:1px solid #475071;border-radius:20px;box-shadow:0 24px 70px #080B2080}
27
+ .th-aurora-dev .codewalk-bar{background:#202A47;color:#F1F2FF;border-bottom-color:#475071}
28
+ .th-aurora-dev .codewalk-editor .code{color:#F1F2FF;border:0;box-shadow:none}
29
+ .th-aurora-dev :is(.codewalk-language,.codewalk-footer,.cn,.tok-comment,.api-code .cl.cm .cc){color:#B5BBD9}
30
+ .th-aurora-dev .codewalk-footer{border-top-color:#475071}
31
+ .th-aurora-dev .code .cl.hl,.th-aurora-dev .api-code .cl.run{background:#C4ADFF1A;box-shadow:inset 4px 0 #C4ADFF;color:#FFF}
32
+ .th-aurora-dev .tok-keyword,.th-aurora-dev .tok-type{color:#C4ADFF}.th-aurora-dev .tok-string,.th-aurora-dev .tok-operator{color:#83E7EF}.th-aurora-dev .tok-number{color:#FFDCA2}.th-aurora-dev .tok-function{color:#A9C8FF}.th-aurora-dev .tok-constant{color:#FFADCA}.th-aurora-dev .tok-punctuation{color:#D9DCF0}
33
+ .th-aurora-dev :is(.api-run,.lesson-controls button,.nav-back){border-radius:14px;font-family:'Space Grotesk',sans-serif}
34
+ .th-aurora-dev :is(.api-run,.lesson-controls button,.api-tabs button,.nav-back):focus-visible{outline:4px solid var(--em);outline-offset:5px}
35
+ .th-aurora-dev .api-tabs button{color:#B5BBD9}.th-aurora-dev .api-tabs button[aria-selected=true]{color:#F1F2FF}
36
+ .th-aurora-dev :is(.api-hint,.api-meta,.api-auth span,.api-tl-i){color:#B5BBD9}
37
+ .th-aurora-dev .lesson-output{border-top:3px solid var(--hi);border-radius:16px}
38
+ html[data-motion="none"] .th-aurora-dev .ambient,.export .th-aurora-dev .ambient{animation:none}
39
+ @media(prefers-reduced-motion:reduce){.th-aurora-dev .ambient{animation:none}}
@@ -0,0 +1,33 @@
1
+ /* Terminal Pro: cabeçalho compacto, moldura de IDE, menta e azul nas partes do código; animação ambiente
2
+ pela rede do runtime, que só anda no slide atual e fica parada na exportação e com movimento reduzido. */
3
+ .th-terminal-pro .orn i:nth-child(1){display:block;inset:0;background:linear-gradient(90deg,color-mix(in srgb,var(--hi) 6%,transparent) 1px,transparent 1px),linear-gradient(color-mix(in srgb,var(--hi) 6%,transparent) 1px,transparent 1px);background-size:64px 64px;mask-image:linear-gradient(90deg,transparent 38%,#000)}
4
+ .th-terminal-pro .orn i:nth-child(2){display:block;right:48px;top:48px;bottom:48px;width:2px;background:linear-gradient(transparent,var(--hi) 35%,var(--em) 65%,transparent);opacity:.35}
5
+ .th-terminal-pro .orn i:nth-child(3){display:block;right:44px;top:52px;width:10px;height:10px;border:2px solid var(--hi);background:var(--bg)}
6
+ .th-terminal-pro .ambient{opacity:.35!important}
7
+ .th-terminal-pro .kicker{align-self:flex-start;padding:12px 18px;background:var(--surface);color:var(--hi);border:1px solid var(--line);border-radius:7px;font-size:20px;letter-spacing:.1em}
8
+ .th-terminal-pro .L-cover .cv-main,.th-terminal-pro .L-end .en-main{border-left:5px solid var(--hi);padding-left:42px;gap:32px}
9
+ .th-terminal-pro .L-cover .ttl{max-width:1330px}
10
+ .th-terminal-pro .L-cover .sub{max-width:1220px}
11
+ .th-terminal-pro .L-section{gap:48px}
12
+ .th-terminal-pro .sc-num{font-family:'IBM Plex Mono',monospace;font-size:156px;line-height:1.2;padding:20px 26px;border:1px solid var(--hi);border-radius:12px;color:var(--hi);background:var(--surface)}
13
+ .th-terminal-pro .hd{padding:0 0 22px 22px;border-left:4px solid var(--hi);border-bottom:1px solid var(--line);gap:16px}
14
+ .th-terminal-pro :is(.card:not(.card-hi),.stat-card,.step-card){border:1px solid var(--line);border-top:4px solid var(--hi);background:var(--surface);border-radius:12px;box-shadow:0 12px 36px #0002}
15
+ .th-terminal-pro .li-n{font-family:'IBM Plex Mono',monospace;background:var(--surface);padding:8px 12px;border:1px solid var(--line);border-radius:8px;color:var(--hi)}
16
+ .th-terminal-pro .li-b{border:0;border-radius:3px;background:var(--hi);width:14px;height:14px}
17
+ .th-terminal-pro .q-text,.th-terminal-pro .st-body{border-left:4px solid var(--hi);padding-left:32px}
18
+ .th-terminal-pro .nb-val,.th-terminal-pro .st-val{font-family:'IBM Plex Mono',monospace;color:var(--hi)}
19
+ /* Código sempre numa superfície escura estável, inclusive com uma paleta clara aplicada por cima. */
20
+ .th-terminal-pro :is(.code,.codewalk-editor,.api-req,.api-res){background:#0D1720;color:#EAF5F1;border:1px solid #344B58;border-radius:12px;box-shadow:0 22px 60px #0003}
21
+ .th-terminal-pro .codewalk-bar{background:#172530;border-bottom-color:#344B58;color:#EAF5F1}
22
+ .th-terminal-pro .codewalk-editor .code{color:#EAF5F1;border:0;box-shadow:none}
23
+ .th-terminal-pro :is(.codewalk-language,.codewalk-footer,.cn,.tok-comment,.api-code .cl.cm .cc){color:#A4B8B4}
24
+ .th-terminal-pro .codewalk-footer{border-top-color:#344B58}
25
+ .th-terminal-pro .code .cl.hl,.th-terminal-pro .api-code .cl.run{background:#74E8B41A;box-shadow:inset 4px 0 #74E8B4;color:#F4FFF9}
26
+ .th-terminal-pro .tok-keyword,.th-terminal-pro .tok-constant{color:#CEB4FF}.th-terminal-pro .tok-string{color:#74E8B4}.th-terminal-pro .tok-number,.th-terminal-pro .tok-type{color:#FFCB8A}.th-terminal-pro .tok-function{color:#83CCF8}.th-terminal-pro .tok-operator,.th-terminal-pro .tok-punctuation{color:#C1D6E2}
27
+ .th-terminal-pro :is(.api-run,.lesson-controls button,.nav-back){border-radius:8px;font-family:'Space Grotesk',sans-serif}
28
+ .th-terminal-pro :is(.api-run,.lesson-controls button,.api-tabs button,.nav-back):focus-visible{outline:4px solid var(--em);outline-offset:5px}
29
+ .th-terminal-pro .api-tabs button{color:#A4B8B4}.th-terminal-pro .api-tabs button[aria-selected=true]{color:#EAF5F1}
30
+ .th-terminal-pro :is(.api-hint,.api-meta,.api-auth span,.api-tl-i){color:#A4B8B4}
31
+ .th-terminal-pro .lesson-output{border-left:4px solid var(--hi);border-radius:8px}
32
+ html[data-motion="none"] .th-terminal-pro .ambient,.export .th-terminal-pro .ambient{animation:none}
33
+ @media(prefers-reduced-motion:reduce){.th-terminal-pro .ambient{animation:none}}