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.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/SKILL.md +148 -0
  4. package/bin/sagadeck.js +511 -0
  5. package/docs/ANDAMENTO.md +1255 -0
  6. package/docs/EXPERIENCIAS.md +39 -0
  7. package/docs/PLANO-EXPERIENCIAS.md +39 -0
  8. package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
  9. package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
  10. package/docs/REFERENCIA.md +1131 -0
  11. package/package.json +59 -0
  12. package/src/adaptive-layouts.js +18 -0
  13. package/src/ai/art-direction.js +8 -0
  14. package/src/ai/commands.js +130 -0
  15. package/src/ai/context.js +245 -0
  16. package/src/ai/deck-ai.js +1135 -0
  17. package/src/ai/directions.js +74 -0
  18. package/src/ai/document-materials.js +49 -0
  19. package/src/ai/document-visuals.js +215 -0
  20. package/src/ai/evaluation.js +31 -0
  21. package/src/ai/ground.js +55 -0
  22. package/src/ai/llm.js +292 -0
  23. package/src/ai/quality.js +51 -0
  24. package/src/ai/relay.js +45 -0
  25. package/src/ai/staged-generation.js +24 -0
  26. package/src/ai/transform.js +858 -0
  27. package/src/ai/usage.js +17 -0
  28. package/src/ai/variety.js +80 -0
  29. package/src/ai/video-frames.js +24 -0
  30. package/src/ai/video-generation.js +126 -0
  31. package/src/ai/web-operation.js +31 -0
  32. package/src/algo-catalog.js +153 -0
  33. package/src/algo-trace.js +139 -0
  34. package/src/api-client.js +514 -0
  35. package/src/api-collections.js +14 -0
  36. package/src/api-demo.js +222 -0
  37. package/src/aspect.js +40 -0
  38. package/src/build.js +323 -0
  39. package/src/carousel.js +56 -0
  40. package/src/chrome.js +65 -0
  41. package/src/code-highlight.js +68 -0
  42. package/src/code-lab-browser.js +22 -0
  43. package/src/code-lab.js +3 -0
  44. package/src/code-language.js +52 -0
  45. package/src/csv.js +74 -0
  46. package/src/decision-lab.js +20 -0
  47. package/src/deck-file.js +119 -0
  48. package/src/diagram/napkin-examples.js +13 -0
  49. package/src/diagram/napkin.js +539 -0
  50. package/src/docx.js +128 -0
  51. package/src/dynamics/git-sim.js +200 -0
  52. package/src/dynamics/layouts.js +112 -0
  53. package/src/elements.js +554 -0
  54. package/src/experiences.js +124 -0
  55. package/src/exploration.js +31 -0
  56. package/src/export/browser.js +53 -0
  57. package/src/export/estudo.js +67 -0
  58. package/src/export/pptx.js +475 -0
  59. package/src/export/roteiro.js +40 -0
  60. package/src/export/shots.js +182 -0
  61. package/src/figures/charts.js +383 -0
  62. package/src/figures/diagrams.js +117 -0
  63. package/src/figures/icons.js +166 -0
  64. package/src/figures/material-pictos.js +20 -0
  65. package/src/figures/pictos.js +255 -0
  66. package/src/figures/points.js +106 -0
  67. package/src/figures/qr.js +29 -0
  68. package/src/figures/ufmap.js +48 -0
  69. package/src/fiscal/autofix.js +377 -0
  70. package/src/fiscal/normalize.js +115 -0
  71. package/src/graph-lab.js +24 -0
  72. package/src/hologram.js +30 -0
  73. package/src/identity.js +86 -0
  74. package/src/import/crop.js +59 -0
  75. package/src/import/index.js +132 -0
  76. package/src/import/legacy-word.js +42 -0
  77. package/src/import/merge.js +45 -0
  78. package/src/import/office.js +123 -0
  79. package/src/import/omml.js +69 -0
  80. package/src/import/pdf-render.js +56 -0
  81. package/src/import/pptx.js +701 -0
  82. package/src/import/xml.js +42 -0
  83. package/src/infographic.js +401 -0
  84. package/src/layouts.js +723 -0
  85. package/src/lessons.js +171 -0
  86. package/src/library.js +399 -0
  87. package/src/markup.js +116 -0
  88. package/src/master.js +214 -0
  89. package/src/mcp/server.js +371 -0
  90. package/src/meeting.js +42 -0
  91. package/src/motion.js +56 -0
  92. package/src/package.js +116 -0
  93. package/src/playground.js +19 -0
  94. package/src/portal-scene.js +20 -0
  95. package/src/preferences.js +61 -0
  96. package/src/protect.js +51 -0
  97. package/src/purpose.js +24 -0
  98. package/src/pytrace.js +1092 -0
  99. package/src/research/duckduckgo.js +73 -0
  100. package/src/research/research.js +202 -0
  101. package/src/research/svg-image.js +17 -0
  102. package/src/research/visuals.js +52 -0
  103. package/src/runtime/api-collection-ui.js +5 -0
  104. package/src/runtime/api-collections.js +28 -0
  105. package/src/runtime/api-core.js +530 -0
  106. package/src/runtime/api-ui.js +966 -0
  107. package/src/runtime/base.css +1774 -0
  108. package/src/runtime/calc.js +139 -0
  109. package/src/runtime/code-lab.js +371 -0
  110. package/src/runtime/decision-lab.js +49 -0
  111. package/src/runtime/diagram.js +341 -0
  112. package/src/runtime/fit.js +268 -0
  113. package/src/runtime/fonts/caveat.css +2 -0
  114. package/src/runtime/fonts/comic-neue.css +3 -0
  115. package/src/runtime/fonts/fredoka.css +2 -0
  116. package/src/runtime/fonts/index.json +10 -0
  117. package/src/runtime/fonts/inter.css +2 -0
  118. package/src/runtime/fonts/patrick-hand.css +2 -0
  119. package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
  120. package/src/runtime/fonts/press-start-2p.css +2 -0
  121. package/src/runtime/fonts/vt323.css +2 -0
  122. package/src/runtime/formula.js +148 -0
  123. package/src/runtime/graph-lab.js +72 -0
  124. package/src/runtime/meeting.css +7 -0
  125. package/src/runtime/meeting.js +10 -0
  126. package/src/runtime/motion.js +15 -0
  127. package/src/runtime/playground.js +11 -0
  128. package/src/runtime/points.js +19 -0
  129. package/src/runtime/portal-scene.css +6 -0
  130. package/src/runtime/portal-scene.js +22 -0
  131. package/src/runtime/runtime.js +827 -0
  132. package/src/runtime/science.js +89 -0
  133. package/src/runtime/skins/aluminio.css +31 -0
  134. package/src/runtime/skins/arcade.css +25 -0
  135. package/src/runtime/skins/aurora.css +17 -0
  136. package/src/runtime/skins/bauhaus.css +19 -0
  137. package/src/runtime/skins/editorial.css +19 -0
  138. package/src/runtime/skins/jornal.css +17 -0
  139. package/src/runtime/skins/manual-noite.css +14 -0
  140. package/src/runtime/skins/manual.css +14 -0
  141. package/src/runtime/skins/noite.css +21 -0
  142. package/src/runtime/skins/oceano.css +17 -0
  143. package/src/runtime/skins/pop.css +23 -0
  144. package/src/runtime/skins/prata.css +18 -0
  145. package/src/runtime/skins/prisma.css +28 -0
  146. package/src/runtime/skins/rabisco.css +18 -0
  147. package/src/runtime/skins/relevo.css +33 -0
  148. package/src/runtime/skins/sinal.css +16 -0
  149. package/src/runtime/skins/terminal.css +23 -0
  150. package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
  151. package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
  152. package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
  153. package/src/runtime/vendor/katex.css +1 -0
  154. package/src/runtime/vendor/mermaid.min.js +3636 -0
  155. package/src/runtime/vendor/plotly.min.js +3620 -0
  156. package/src/runtime/video-player.js +35 -0
  157. package/src/science.js +162 -0
  158. package/src/studio/ai-response.js +36 -0
  159. package/src/studio/api-routes.js +152 -0
  160. package/src/studio/assets/contexto/leia-me.md +12 -0
  161. package/src/studio/assets/contexto/vendas.csv +7 -0
  162. package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
  163. package/src/studio/assets/imagens/colecao-cor.png +0 -0
  164. package/src/studio/assets/imagens/colecao-equipe.png +0 -0
  165. package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
  166. package/src/studio/code-routes.js +32 -0
  167. package/src/studio/command-approval.js +48 -0
  168. package/src/studio/demo-decks.js +98 -0
  169. package/src/studio/direction-routes.js +29 -0
  170. package/src/studio/errors.js +65 -0
  171. package/src/studio/exploration-demo.js +17 -0
  172. package/src/studio/exporting.js +105 -0
  173. package/src/studio/generation-report.js +10 -0
  174. package/src/studio/instance.js +48 -0
  175. package/src/studio/layout-samples.js +152 -0
  176. package/src/studio/project.js +371 -0
  177. package/src/studio/public/api-collections.js +28 -0
  178. package/src/studio/public/app.js +5265 -0
  179. package/src/studio/public/art-preview.js +10 -0
  180. package/src/studio/public/calc-fields.js +11 -0
  181. package/src/studio/public/chat-progress.js +47 -0
  182. package/src/studio/public/developer-fields.js +22 -0
  183. package/src/studio/public/explorer.js +443 -0
  184. package/src/studio/public/history.js +58 -0
  185. package/src/studio/public/index.html +867 -0
  186. package/src/studio/public/inspector.js +270 -0
  187. package/src/studio/public/lab-fields.js +5 -0
  188. package/src/studio/public/library.css +275 -0
  189. package/src/studio/public/library.html +55 -0
  190. package/src/studio/public/library.js +471 -0
  191. package/src/studio/public/merge-decks.js +155 -0
  192. package/src/studio/public/review-ui.js +100 -0
  193. package/src/studio/public/screenshot-editor.js +140 -0
  194. package/src/studio/public/share-ui.js +72 -0
  195. package/src/studio/public/slide-clipboard.js +23 -0
  196. package/src/studio/public/slide-form.js +1263 -0
  197. package/src/studio/public/studio-awareness.js +154 -0
  198. package/src/studio/public/studio-next.css +303 -0
  199. package/src/studio/public/style.css +1262 -0
  200. package/src/studio/public/ui-icons.js +2 -0
  201. package/src/studio/public/viewers.js +372 -0
  202. package/src/studio/public/visual-editor.js +326 -0
  203. package/src/studio/public/workspace.js +18 -0
  204. package/src/studio/public-files.js +1 -0
  205. package/src/studio/request-helpers.js +41 -0
  206. package/src/studio/server.js +1583 -0
  207. package/src/studio/share-routes.js +146 -0
  208. package/src/studio/slide-copy-routes.js +26 -0
  209. package/src/studio/slide-copy.js +30 -0
  210. package/src/studio/snapshot.js +160 -0
  211. package/src/studio/style-routes.js +103 -0
  212. package/src/studio/template-collections.js +97 -0
  213. package/src/studio/versions.js +95 -0
  214. package/src/studio/video-download.js +29 -0
  215. package/src/studio/visual-keys.js +52 -0
  216. package/src/studio/workspaces.js +39 -0
  217. package/src/table.js +93 -0
  218. package/src/templates/scaffold.js +372 -0
  219. package/src/themes.js +604 -0
  220. package/src/trace-view.js +450 -0
  221. package/src/uid.js +19 -0
  222. package/src/video-player.js +40 -0
  223. package/src/visual-edits.js +99 -0
  224. package/src/ws.js +134 -0
  225. package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
  226. package/templates/cenario/imagens/cidade.svg +30 -0
  227. package/templates/cenario/imagens/pessoa.svg +15 -0
  228. package/templates/cenario-e-ciencia.yaml +47 -0
  229. package/templates/ensaio-api.yaml +160 -0
  230. package/templates/exemplo-alegre.yaml +75 -0
  231. package/templates/exemplo-keynote.yaml +90 -0
  232. package/templates/exemplo-napkin.yaml +139 -0
  233. package/templates/exemplo.yaml +228 -0
@@ -0,0 +1,95 @@
1
+ // Versões do deck: cada salvamento guarda uma cópia em .sagadeck/versoes/ (últimas 10,
2
+ // sem duplicar salvamento idêntico). A pessoa compara lado a lado e restaura se quiser.
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import YAML from "yaml";
6
+
7
+ const MAX = 10;
8
+
9
+ export function versionsDir(file) {
10
+ return path.join(path.dirname(path.resolve(file)), ".sagadeck", "versoes");
11
+ }
12
+
13
+ const stamp = (d = new Date()) =>
14
+ `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}${String(d.getSeconds()).padStart(2, "0")}`;
15
+
16
+ // fotografa o YAML atual; nunca quebra o salvamento (best-effort)
17
+ export function snapshotVersion(file, text) {
18
+ try {
19
+ if (!file || typeof text !== "string" || !text.trim()) return null;
20
+ const dir = versionsDir(file);
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".yaml")).sort();
23
+ if (files.length) {
24
+ const last = fs.readFileSync(path.join(dir, files[files.length - 1]), "utf8");
25
+ if (last === text) return null; // nada mudou desde a última
26
+ }
27
+ let name = `${stamp()}.yaml`;
28
+ for (let n = 2; fs.existsSync(path.join(dir, name)); n++) name = `${stamp()}-${n}.yaml`;
29
+ fs.writeFileSync(path.join(dir, name), text, "utf8");
30
+ for (const old of files.slice(0, Math.max(0, files.length + 1 - MAX))) {
31
+ try { fs.unlinkSync(path.join(dir, old)); } catch {}
32
+ }
33
+ return name;
34
+ } catch { return null; }
35
+ }
36
+
37
+ export function listVersions(file) {
38
+ try {
39
+ const dir = versionsDir(file);
40
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".yaml")).sort().reverse().map((name) => {
41
+ const st = fs.statSync(path.join(dir, name));
42
+ return { name, at: new Date(st.mtimeMs).toISOString(), bytes: st.size };
43
+ });
44
+ } catch { return []; }
45
+ }
46
+
47
+ export function readVersion(file, name) {
48
+ const raw = String(name || "");
49
+ if (raw.includes("..") || raw.includes("/") || raw.includes("\\")) throw new Error("Versão inválida.");
50
+ const base = path.basename(raw);
51
+ if (!/^[\w.-]+\.yaml$/.test(base)) throw new Error("Versão inválida.");
52
+ try { return fs.readFileSync(path.join(versionsDir(file), base), "utf8"); }
53
+ catch { throw new Error("Versão não encontrada."); }
54
+ }
55
+
56
+ // quais slides mudaram entre duas listas (por índice; adicionado/removido conta como mudança)
57
+ export function diffSlides(a, b) {
58
+ const A = Array.isArray(a) ? a : [], B = Array.isArray(b) ? b : [];
59
+ const n = Math.max(A.length, B.length), changed = [];
60
+ for (let i = 0; i < n; i++) {
61
+ if (JSON.stringify(A[i] ?? null) !== JSON.stringify(B[i] ?? null)) changed.push(i);
62
+ }
63
+ return changed;
64
+ }
65
+
66
+ export async function versionRoutes({ req, res, pathname, W, readJSON }) {
67
+ const send = (status, data) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); };
68
+ if (pathname === "/api/versions" && req.method === "GET") {
69
+ if (!W?.file) return send(400, { error: "Abra uma apresentação primeiro." }), true;
70
+ send(200, { versions: listVersions(W.file) });
71
+ return true;
72
+ }
73
+ if (pathname === "/api/versions/show" && req.method === "POST") {
74
+ if (!W?.file) return send(400, { error: "Abra uma apresentação primeiro." }), true;
75
+ try {
76
+ const b = await readJSON(req);
77
+ const yaml = readVersion(W.file, b.name);
78
+ const spec = YAML.parse(yaml) || {};
79
+ send(200, { yaml, slides: Array.isArray(spec.slides) ? spec.slides : [], theme: spec.theme || null, palette: spec.palette || null });
80
+ } catch (e) { send(400, { error: e.message }); }
81
+ return true;
82
+ }
83
+ if (pathname === "/api/versions/restore" && req.method === "POST") {
84
+ if (!W?.file) return send(400, { error: "Abra uma apresentação primeiro." }), true;
85
+ try {
86
+ const b = await readJSON(req);
87
+ const yaml = readVersion(W.file, b.name);
88
+ snapshotVersion(W.file, fs.readFileSync(W.file, "utf8")); // o estado atual vira versão antes de voltar
89
+ fs.writeFileSync(W.file, yaml, "utf8");
90
+ send(200, { ok: true });
91
+ } catch (e) { send(400, { error: e.message }); }
92
+ return true;
93
+ }
94
+ return false;
95
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { spawn } from 'node:child_process';
5
+ import { youtubeId } from '../video-player.js';
6
+
7
+ export async function downloadYoutube(url, dir, {spawnProcess=spawn, timeoutMs=180000, ffmpeg: suppliedFfmpeg}={}) {
8
+ const id=youtubeId(url);if(!id)throw new Error('Informe um link válido do YouTube.');
9
+ const folder=path.join(dir,'videos');fs.mkdirSync(folder,{recursive:true});
10
+ const file=path.join(folder,`${id}-${crypto.randomBytes(4).toString('hex')}.mp4`);
11
+ const local = process.platform === 'win32' && process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA,'SagaDeck','tools','yt-dlp.exe') : '';
12
+ const executable=process.env.SAGADECK_YTDLP || (local && fs.existsSync(local) ? local : 'yt-dlp');
13
+ const bin = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA,'SagaDeck','python','imageio_ffmpeg','binaries') : '';
14
+ const ffmpeg = suppliedFfmpeg !== undefined ? suppliedFfmpeg : process.env.SAGADECK_FFMPEG || (bin && fs.existsSync(bin) ? fs.readdirSync(bin).filter(n=>/^ffmpeg.*\.exe$/.test(n)).map(n=>path.join(bin,n))[0] : undefined);
15
+ // sem ffmpeg não dá para juntar vídeo+áudio: baixa só o vídeo (mudo, ideal para loop de fundo)
16
+ const fmt = ffmpeg ? 'bestvideo[ext=mp4][vcodec^=avc1][height<=720]+bestaudio[ext=m4a]/best[ext=mp4][height<=720]' : 'bv[ext=mp4][height<=720]';
17
+ const args=['--ignore-config','--no-playlist','--no-overwrites','--max-filesize','100M','--socket-timeout','15','--retries','1','--js-runtimes',`node:${process.execPath}`,'-f',fmt,'--merge-output-format','mp4',...(ffmpeg?['--ffmpeg-location',ffmpeg]:[]),'-o',file,'--',`https://www.youtube.com/watch?v=${id}`];
18
+ await new Promise((resolve,reject)=>{
19
+ const child=spawnProcess(executable,args,{cwd:dir,windowsHide:true,stdio:['ignore','ignore','pipe']});let error='',settled=false;
20
+ const finish=(e)=>{if(settled)return;settled=true;clearTimeout(timer);e?reject(e):resolve()};
21
+ const timer=setTimeout(()=>{child.kill();finish(new Error('O download excedeu três minutos.'))},timeoutMs);
22
+ child.stderr?.on('data',d=>{error=(error+d.toString()).slice(-2000)});
23
+ child.on('error',e=>finish(new Error(e.code==='ENOENT'?'yt-dlp não instalado. Configure SAGADECK_YTDLP com o caminho do executável.':e.message)));
24
+ child.on('close',code=>finish(code===0?null:new Error(error||`Download falhou (${code}).`)));
25
+ });
26
+ if(!fs.existsSync(file)||!fs.statSync(file).size)throw new Error('O YouTube não forneceu um arquivo MP4.');
27
+ if(fs.statSync(file).size>100*1024*1024){fs.unlinkSync(file);throw new Error('Use um vídeo menor que 100 MB.');}
28
+ return {url:path.relative(dir,file).replace(/\\/g,'/'),source:`https://www.youtube.com/watch?v=${id}`};
29
+ }
@@ -0,0 +1,52 @@
1
+ // Ajustes visuais (visualEdits) presos ao objeto certo, venha a mudança de onde vier (a pessoa, o formulário, a IA):
2
+ // - migração: chave antiga (grupo-N, pela ordem) vira a chave pelo conteúdo (grupo~impressão), deck antigo sem perda
3
+ // - continuidade: o texto de um objeto mudou (a chave pelo conteúdo muda junto): se no grupo sumiu exatamente um
4
+ // objeto e apareceu exatamente um, o ajuste passa para ele. Inserir ou apagar OUTRO objeto não mexe em nada.
5
+ import { renderSlide } from "../build.js";
6
+
7
+ const LEGACY = /^[\w-]+-\d+$/;
8
+ function keysOf(slide, i, spec) {
9
+ try {
10
+ const { html } = renderSlide(slide, i, spec);
11
+ return [...html.matchAll(/data-vkey="([^"]+)"(?: data-vkey-old="([^"]+)")?/g)].map((m) => ({ key: m[1], old: m[2] || null }));
12
+ } catch { return null; }
13
+ }
14
+ const groupOf = (k) => String(k).split("~")[0];
15
+ const content = (s) => { if (!s || typeof s !== "object") return ""; const { visualEdits, uid, review, ...rest } = s; return JSON.stringify(rest); };
16
+
17
+ export function migrateLegacyKeys(spec) {
18
+ let moved = 0;
19
+ (spec?.slides || []).forEach((s, i) => {
20
+ const ve = s?.visualEdits;
21
+ if (!ve || !Object.keys(ve).some((k) => LEGACY.test(k) && !k.includes("~"))) return;
22
+ const keys = keysOf(s, i, spec);
23
+ if (!keys) return;
24
+ const next = { ...ve };
25
+ for (const { key, old } of keys) if (old && next[old] && !next[key]) { next[key] = next[old]; delete next[old]; moved++; }
26
+ s.visualEdits = next;
27
+ });
28
+ return moved;
29
+ }
30
+
31
+ export function carryVisualEdits(prev, next) {
32
+ let carried = 0;
33
+ const before = new Map((prev?.slides || []).filter((s) => s?.uid).map((s, i) => [s.uid, { s, i }]));
34
+ (next?.slides || []).forEach((s, i) => {
35
+ const ve = s?.visualEdits;
36
+ const p = s?.uid && before.get(s.uid);
37
+ if (!ve || !p || content(p.s) === content(s)) return;
38
+ const a = keysOf(p.s, p.i, prev), b = keysOf(s, i, next);
39
+ if (!a || !b) return;
40
+ const nowKeys = new Set(b.map((x) => x.key)), wasKeys = new Set(a.map((x) => x.key));
41
+ const out = { ...ve };
42
+ for (const k of Object.keys(ve)) {
43
+ if (nowKeys.has(k) || !k.includes("~")) continue;
44
+ const g = groupOf(k);
45
+ const gone = [...wasKeys].filter((x) => groupOf(x) === g && !nowKeys.has(x));
46
+ const came = [...nowKeys].filter((x) => groupOf(x) === g && !wasKeys.has(x));
47
+ if (gone.length === 1 && came.length === 1 && gone[0] === k && !out[came[0]]) { out[came[0]] = out[k]; delete out[k]; carried++; }
48
+ }
49
+ s.visualEdits = out;
50
+ });
51
+ return carried;
52
+ }
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { loadSpec } from "../build.js";
4
+ export function scopePreviewHTML(html, req) {
5
+ const tab = String(req.headers['x-sagadeck-workspace'] || new URL(req.url, 'http://localhost').searchParams.get('_workspace') || '').slice(0, 100);
6
+ if (!tab) return html;
7
+ const value = JSON.stringify(tab).replace(/</g, '\\u003c');
8
+ return html.replace(/<head>/i, `<head><script>(function(){const id=${value},original=window.fetch.bind(window);window.fetch=(input,options)=>{const u=new URL(typeof input==='string'||input instanceof URL?input:input.url,location.href);if(u.origin!==location.origin)return original(input,options);const headers=new Headers(options?.headers||input?.headers);headers.set('x-sagadeck-workspace',id);return original(input,{...options,headers})}})();</script>`);
9
+ }
10
+ export function workspaceResolver({ opts, USER_HEADER, workspaces, newWorkspace }) {
11
+ function freshWorkspace(W) {
12
+ if (W.file && fs.existsSync(W.file)) {
13
+ const stat = fs.statSync(W.file); if (!stat.isFile()) return W;
14
+ const stamp = W.file+'|'+stat.mtimeMs+'|'+stat.size;
15
+ if (W.diskStamp !== stamp) { W.spec = loadSpec(W.file); W.diskStamp = stamp; }
16
+ }
17
+ return W;
18
+ }
19
+ function workspaceOf(req) {
20
+ const user = opts.multiuser ? String(req.headers[USER_HEADER] || "").trim() : "";
21
+ if (opts.multiuser && !user) return null;
22
+ if (!workspaces.has(user)) workspaces.set(user, newWorkspace(user));
23
+ const base = workspaces.get(user);
24
+ const tab = String(req.headers['x-sagadeck-workspace'] || new URL(req.url, 'http://localhost').searchParams.get('_workspace') || '').slice(0, 100);
25
+ if (!tab) return freshWorkspace(base);
26
+ const key = JSON.stringify([user, tab]);
27
+ if (!workspaces.has(key)) workspaces.set(key, { ...base, spec: structuredClone(base.spec), aiChats: new Map(), contextDocs: new Map() });
28
+ return freshWorkspace(workspaces.get(key));
29
+ }
30
+ return workspaceOf;
31
+ }
32
+
33
+ const sameFile = (a, b) => { const n = (x) => path.resolve(String(x)); return process.platform === "win32" ? n(a).toLowerCase() === n(b).toLowerCase() : n(a) === n(b); };
34
+ export function staleTab(body, W, res) {
35
+ if (!body?.expectFile || !W.file || sameFile(body.expectFile, W.file)) return false;
36
+ res.writeHead(409, { "Content-Type": "application/json; charset=utf-8" });
37
+ res.end(JSON.stringify({ error: "Outra aba ou aparelho abriu outra apresentação neste Studio. Recarregue a página para continuar; esta mudança não foi gravada.", stale: true, file: W.file }));
38
+ return true;
39
+ }
package/src/table.js ADDED
@@ -0,0 +1,93 @@
1
+ // Tabela de verdade (não figura): cabeçalho na cor do tema, números alinhados à direita com algarismos de mesma
2
+ // largura, linha zebrada, destaque de linha/coluna/célula e linha de total. Serve ao layout `table` e ao elemento
3
+ // `table` em qualquer lugar (conteúdo, canvas, add). Estilos: faixa (padrão), zebra, linhas, colunas (cada coluna
4
+ // com uma cor da paleta) e cartão.
5
+ import { md, esc } from "./markup.js";
6
+ import { parseCSV } from "./csv.js";
7
+ import { col } from "./themes.js";
8
+
9
+ const STYLES = new Set(["faixa", "zebra", "linhas", "colunas", "cartao"]);
10
+ const ALIGN = new Set(["left", "center", "right"]);
11
+ // "1.234,5", "40%", "R$ 12", "-3,2", "10 mm", "2.218,0 m³/s": número (alinha à direita)
12
+ const NUMERIC = /^\s*[-+−]?\s*(R\$|US\$|€|\$)?\s*[-+−]?\d[\d.,\s]*(%|‰|[a-zA-Zµ°³²/]{0,6})?\s*$/;
13
+ const colorVar = (c) => {
14
+ if (!c) return null;
15
+ const v = String(c).trim();
16
+ if (/^c[1-5]$/.test(v)) return `var(--c-${v})`;
17
+ if (["hi", "em", "fg", "muted", "line", "surface"].includes(v)) return `var(--${v})`;
18
+ if (/^#?[0-9a-f]{6}$/i.test(v)) return `#${v.replace("#", "")}`;
19
+ if (/^(accent|alert|ink|paper)$/.test(v)) return `var(--c-${v})`;
20
+ return null;
21
+ };
22
+
23
+ // a cor real (hex) no tom claro do tema e o texto que se lê em cima dela (branco ou a tinta do tema)
24
+ function hexOf(theme, c) {
25
+ if (!theme) return null;
26
+ const tone = theme.tones?.light || {};
27
+ const v = !c ? tone.em : ["hi", "em"].includes(c) ? tone[c] : c;
28
+ const h = String(col(theme, v) || "").replace("#", "");
29
+ return /^[0-9a-f]{6}$/i.test(h) ? h : null;
30
+ }
31
+ function onColor(theme, hex) {
32
+ if (!hex) return null;
33
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
34
+ const L = 0.2126 * r + 0.7152 * g + 0.0722 * b;
35
+ return L > 0.42 ? "#16181d" : "#ffffff";
36
+ }
37
+
38
+ // o que veio (head/cols/header + rows; rows de objetos; csv colado) → { head: [...] | null, rows: [[...]] }
39
+ export function tableModel(t = {}) {
40
+ let head = Array.isArray(t.head) ? t.head : Array.isArray(t.cols) && t.cols.every((c) => typeof c !== "number") ? t.cols : null;
41
+ let rows = Array.isArray(t.rows) ? t.rows : [];
42
+ if (typeof t.csv === "string" && t.csv.trim()) {
43
+ const parsed = parseCSV(t.csv).rows.filter((r) => r.some((c) => String(c).trim()));
44
+ if (!head && t.header !== false) { head = parsed[0]; rows = parsed.slice(1); } else rows = parsed;
45
+ }
46
+ if (rows.length && rows.every((r) => r && typeof r === "object" && !Array.isArray(r))) {
47
+ const keys = [...new Set(rows.flatMap((r) => Object.keys(r)))];
48
+ head = head || keys;
49
+ rows = rows.map((r) => keys.map((k) => r[k] ?? ""));
50
+ }
51
+ if (!head && t.header === true && rows.length) { head = rows[0]; rows = rows.slice(1); }
52
+ rows = rows.map((r) => (Array.isArray(r) ? r : [r]).map((c) => (c == null ? "" : c)));
53
+ const n = Math.max(head?.length || 0, ...rows.map((r) => r.length), 0);
54
+ rows = rows.map((r) => [...r, ...Array(Math.max(0, n - r.length)).fill("")]);
55
+ if (head) head = [...head, ...Array(Math.max(0, n - head.length)).fill("")];
56
+ return { head, rows, n };
57
+ }
58
+
59
+ export function tableHTML(t = {}, { size, cls = "", style = "", theme } = {}) {
60
+ const { head, rows, n } = tableModel(t);
61
+ if (!n) throw new Error("tabela vazia: dê `rows` (lista de linhas) e, se quiser, `head` (os títulos das colunas)");
62
+ const kind = STYLES.has(t.style) ? t.style : "faixa";
63
+ // alinhamento: o pedido, senão número à direita e texto à esquerda (pela maioria da coluna)
64
+ const align = Array.from({ length: n }, (_, j) => {
65
+ const want = Array.isArray(t.align) ? t.align[j] : typeof t.align === "string" ? t.align : null;
66
+ if (ALIGN.has(want)) return want;
67
+ const vals = rows.map((r) => String(r[j]).trim()).filter(Boolean);
68
+ return vals.length && vals.filter((v) => NUMERIC.test(v)).length / vals.length >= 0.6 ? "right" : "left";
69
+ });
70
+ // letra pelo tamanho da tabela (o ajuste para caber ainda encolhe se faltar lugar)
71
+ const cells = rows.length * n;
72
+ const fs = Number(size || t.size) || (cells <= 12 ? 50 : cells <= 30 ? 44 : cells <= 60 ? 36 : cells <= 120 ? 28 : 22);
73
+ const hl = t.highlight || {};
74
+ const hlRows = new Set((Array.isArray(hl.rows) ? hl.rows : hl.row != null ? [hl.row] : Array.isArray(t.highlight) ? t.highlight : []).map(Number));
75
+ const hlCols = new Set((Array.isArray(hl.cols) ? hl.cols : hl.col != null ? [hl.col] : []).map(Number));
76
+ const hlCell = Array.isArray(hl.cell) ? hl.cell.map(Number) : null;
77
+ const total = t.total === true;
78
+ const color = colorVar(t.color);
79
+ // texto do cabeçalho legível sobre a cor (amarelo e dourado pedem texto escuro)
80
+ const on = onColor(theme, hexOf(theme, /^c[1-5]$|^#?[0-9a-f]{6}$|^(hi|em|accent|alert)$/i.test(String(t.color || "")) ? String(t.color) : null));
81
+ const vars = `${color ? `--tb-c:${color};` : ""}${on ? `--on-tb:${on};` : ""}font-size:${fs}px;`;
82
+ const kOn = (j) => (kind === "colunas" ? onColor(theme, hexOf(theme, `c${(j % 5) + 1}`)) : null);
83
+ const cell = (v, j, tag, extra = "") => `<${tag} class="${align[j] === "right" ? "num" : align[j] === "center" ? "mid" : ""}${extra}">${md(String(v))}</${tag}>`;
84
+ const wsum = Array.isArray(t.widths) ? t.widths.reduce((a, b) => a + (Number(b) || 1), 0) : 0;
85
+ const colgroup = Array.isArray(t.widths) && t.widths.length === n ? `<colgroup>${t.widths.map((w) => `<col style="width:${((100 * (Number(w) || 1)) / wsum).toFixed(2)}%">`).join("")}</colgroup>` : "";
86
+ const thead = head ? `<thead><tr>${head.map((h, j) => { const c = cell(h, j, "th", `${kind === "colunas" ? ` tb-k${(j % 5) + 1}` : ""}${hlCols.has(j + 1) ? " hl" : ""}`); return kOn(j) ? c.replace("<th ", `<th style="color:${kOn(j)}" `) : c; }).join("")}</tr></thead>` : "";
87
+ const tbody = `<tbody>${rows.map((r, i) => `<tr class="${hlRows.has(i + 1) ? "hl" : ""}${total && i === rows.length - 1 ? " tot" : ""}">${r.map((v, j) => cell(v, j, j === 0 && t.rowHeader ? "th" : "td", `${hlCols.has(j + 1) ? " hl" : ""}${hlCell && hlCell[0] === i + 1 && hlCell[1] === j + 1 ? " hlc" : ""}`)).join("")}</tr>`).join("")}</tbody>`;
88
+ // data-fit: não coube (colunas demais, conclusão ao lado), a letra encolhe até caber (src/runtime/fit.js)
89
+ return `<div class="dtable-wrap tb-${kind}${cls ? ` ${cls}` : ""}" data-fit data-fit-self style="${vars}${style}"><table class="dtable f-body">${colgroup}${thead}${tbody}</table></div>`;
90
+ }
91
+
92
+ export const TABLE_STYLES = [...STYLES];
93
+ void esc;