sagadeck 1.7.1 → 1.9.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 (105) hide show
  1. package/README.md +55 -4
  2. package/SKILL.md +10 -3
  3. package/bin/sagadeck.js +23 -0
  4. package/docs/ANDAMENTO.md +483 -1
  5. package/docs/PLANO-MELHORIAS.md +180 -0
  6. package/docs/REFERENCIA.md +184 -105
  7. package/package.json +34 -2
  8. package/src/ai/commands.js +2 -2
  9. package/src/ai/deck-ai.js +14 -5
  10. package/src/ai/ia-check.js +127 -0
  11. package/src/ai/ia-config.js +2 -2
  12. package/src/ai/llm.js +5 -1
  13. package/src/ai/map-analysis-run.js +119 -0
  14. package/src/ai/map-data.js +221 -0
  15. package/src/ai/quality.js +1 -1
  16. package/src/ai/video-generation.js +70 -25
  17. package/src/build.js +51 -15
  18. package/src/elements.js +39 -1
  19. package/src/export/browser.js +23 -1
  20. package/src/export/pptx.js +9 -3
  21. package/src/export/shots.js +10 -3
  22. package/src/fiscal/normalize.js +19 -0
  23. package/src/fs-retry.js +37 -2
  24. package/src/import/office.js +5 -4
  25. package/src/layouts.js +114 -99
  26. package/src/lessons.js +1 -1
  27. package/src/library.js +7 -7
  28. package/src/map-analysis.js +109 -0
  29. package/src/map-config.js +89 -0
  30. package/src/map-routing.js +82 -0
  31. package/src/map-services.js +60 -0
  32. package/src/map.js +274 -0
  33. package/src/mcp/server.js +17 -0
  34. package/src/motion.js +20 -6
  35. package/src/purpose.js +1 -1
  36. package/src/runtime/base.css +105 -329
  37. package/src/runtime/fit.js +3 -2
  38. package/src/runtime/fonts/amatic-sc.css +3 -0
  39. package/src/runtime/fonts/bebas-neue.css +2 -0
  40. package/src/runtime/fonts/cabin-sketch.css +2 -0
  41. package/src/runtime/fonts/fraunces.css +3 -0
  42. package/src/runtime/fonts/gelasio.css +3 -0
  43. package/src/runtime/fonts/ibm-plex-mono.css +4 -0
  44. package/src/runtime/fonts/index.json +10 -1
  45. package/src/runtime/fonts/itim.css +2 -0
  46. package/src/runtime/fonts/montserrat.css +2 -0
  47. package/src/runtime/fonts/space-grotesk.css +2 -0
  48. package/src/runtime/map-background.js +91 -0
  49. package/src/runtime/map.js +266 -0
  50. package/src/runtime/runtime.js +81 -100
  51. package/src/runtime/skins/caderno.css +33 -0
  52. package/src/runtime/skins/cartaz.css +27 -0
  53. package/src/runtime/skins/infografico.css +29 -0
  54. package/src/runtime/skins/lousa.css +22 -0
  55. package/src/runtime/skins/musgo.css +42 -0
  56. package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
  57. package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
  58. package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
  59. package/src/runtime/vendor/geo-contornos.json +1 -0
  60. package/src/runtime/vendor/leaflet.css +661 -0
  61. package/src/runtime/vendor/leaflet.js +6 -0
  62. package/src/runtime/vendor/maplibre-gl.css +1 -0
  63. package/src/runtime/vendor/maplibre-gl.js +825 -0
  64. package/src/runtime/vendor/maplibre-leaflet.js +234 -0
  65. package/src/runtime/vendor/maplibre-worker.js +10 -0
  66. package/src/studio/ai-chat-route.js +20 -0
  67. package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
  68. package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
  69. package/src/studio/command-approval.js +2 -3
  70. package/src/studio/demo-decks.js +6 -2
  71. package/src/studio/export-jobs.js +86 -0
  72. package/src/studio/exporting.js +36 -18
  73. package/src/studio/feature-demos.js +144 -0
  74. package/src/studio/geo-routes.js +138 -0
  75. package/src/studio/ia-routes.js +9 -0
  76. package/src/studio/layout-samples.js +78 -16
  77. package/src/studio/library-routes.js +0 -9
  78. package/src/studio/project.js +2 -1
  79. package/src/studio/public/ai-settings.js +6 -3
  80. package/src/studio/public/app.js +46 -52
  81. package/src/studio/public/explorer.js +23 -8
  82. package/src/studio/public/export-status.js +154 -0
  83. package/src/studio/public/index.html +17 -10
  84. package/src/studio/public/library.css +12 -3
  85. package/src/studio/public/library.html +3 -0
  86. package/src/studio/public/library.js +39 -12
  87. package/src/studio/public/map-data.js +158 -0
  88. package/src/studio/public/map-editor.js +676 -0
  89. package/src/studio/public/map-fields.js +21 -0
  90. package/src/studio/public/map-settings.js +86 -0
  91. package/src/studio/public/slide-form.js +19 -16
  92. package/src/studio/public/style.css +58 -0
  93. package/src/studio/public/ui-icons.js +1 -1
  94. package/src/studio/public/viewers.js +2 -1
  95. package/src/studio/public/visual-editor.js +1 -1
  96. package/src/studio/server.js +34 -9
  97. package/src/studio/snapshot.js +8 -4
  98. package/src/studio/template-collections.js +1 -1
  99. package/src/studio/tour.js +158 -0
  100. package/src/themes.js +154 -0
  101. package/templates/cenario-e-ciencia.yaml +1 -21
  102. package/templates/ensaio-api.yaml +3 -3
  103. package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
  104. package/templates/cenario/imagens/cidade.svg +0 -30
  105. package/templates/cenario/imagens/pessoa.svg +0 -15
@@ -0,0 +1,154 @@
1
+ // A mesma fila acompanha exportações no editor e na biblioteca, inclusive ao voltar à página.
2
+ (function () {
3
+ 'use strict';
4
+ const labels = { pptx: 'PowerPoint', pdf: 'PDF', roteiro: 'Roteiro', estudo: 'Material de estudo', 'estudo-html': 'Material de estudo', tudo: 'PowerPoint, PDF e roteiro', sagadeck: 'Apresentação' };
5
+ const jobs = new Map(), rows = new Map(), automatic = new Set(), dismissed = new Set();
6
+ let panel, list, heading, toggle, hint, minimized = false, busy = false;
7
+ const elapsed = ms => { const s = Math.max(0, Math.floor(ms / 1000)); return s < 60 ? `${s} s` : `${Math.floor(s / 60)} min ${s % 60} s`; };
8
+ async function json(url, options) {
9
+ let res;
10
+ try { res = await fetch(url, { ...options, signal: AbortSignal.timeout(15000) }); }
11
+ catch (e) {
12
+ if (['TimeoutError', 'AbortError'].includes(e.name)) throw new Error('O Studio não respondeu a tempo. Se a geração começou, ela aparecerá nesta lista quando a conexão voltar.');
13
+ throw new Error('Não foi possível conectar ao Studio. Confira a conexão e tente novamente.');
14
+ }
15
+ let body; try { body = await res.json(); } catch { throw new Error('O servidor não respondeu como esperado. Tente novamente.'); }
16
+ if (!res.ok) throw new Error(body.error || `Não foi possível concluir o pedido (${res.status}).`);
17
+ return body;
18
+ }
19
+ function mount() {
20
+ if (panel) return;
21
+ const style = document.createElement('style');
22
+ style.textContent = `
23
+ #export-status{position:fixed;right:22px;bottom:22px;z-index:40;width:min(390px,calc(100vw - 32px));background:var(--chrome,#fff);color:var(--text,#242424);border:1px solid var(--ui-line,#ddd);border-radius:12px;box-shadow:var(--shadow-3,0 8px 32px #0002);font:13px/1.5 system-ui,sans-serif}
24
+ body:not(.lib) #export-status[data-minimized=true]{top:64px;bottom:auto}
25
+ #export-status[hidden],#export-status [hidden]{display:none!important}
26
+ #export-status header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;gap:12px}
27
+ #export-status button{font:inherit;cursor:pointer;border:1px solid var(--ui-line,#ddd);border-radius:6px;padding:4px 9px;background:transparent;color:inherit}
28
+ #export-status button:focus-visible{outline:2px solid var(--brand,#0f6cbd);outline-offset:2px}
29
+ #export-status button:disabled{opacity:.6;cursor:wait}
30
+ #export-status .export-list{max-height:330px;overflow:auto;padding:0 14px}
31
+ #export-status article{padding:12px 0;border-top:1px solid var(--ui-line,#ddd)}
32
+ #export-status .export-name{display:block;overflow-wrap:anywhere}
33
+ #export-status p{margin:4px 0}
34
+ #export-status progress{display:block;width:100%;height:8px;margin:8px 0;accent-color:var(--brand,#0f6cbd)}
35
+ #export-status .export-actions{display:flex;align-items:center;gap:8px;margin-top:8px}
36
+ #export-status .export-download{background:var(--brand-fill,#0f6cbd);color:var(--on-brand,#fff);border-color:var(--brand-fill,#0f6cbd)}
37
+ #export-status .export-error{color:var(--err,#b42318)}
38
+ #export-status small{opacity:.8}
39
+ #export-status .export-hint{padding:8px 14px 12px;font-size:11px;margin:0}
40
+ #export-status details{margin-top:6px;overflow-wrap:anywhere}
41
+ `;
42
+ document.head.append(style);
43
+ panel = document.createElement('section'); panel.id = 'export-status'; panel.setAttribute('aria-label', 'Exportações');
44
+ panel.innerHTML = '<header><strong>Exportações</strong><button type="button" aria-expanded="true">Recolher</button></header><div class="export-list"></div><p class="export-hint">Você pode continuar trabalhando e baixar depois pela biblioteca. Arquivos disponíveis por até 24 h, enquanto o Studio estiver ligado (10 mais recentes).</p>';
45
+ document.body.append(panel);
46
+ heading = panel.querySelector('strong'); list = panel.querySelector('.export-list'); toggle = panel.querySelector('header button'); hint = panel.querySelector('.export-hint');
47
+ toggle.onclick = () => { minimized = !minimized; render(); };
48
+ }
49
+ function message(j) {
50
+ if (j.status === 'failed') return (j.id.startsWith('starting-') ? 'Não deu para iniciar: ' : 'Falhou: ') + j.error;
51
+ if (j.status === 'ready') return 'Pronto para baixar';
52
+ if (j.status === 'expired') return 'Arquivo não disponível. Gere novamente.';
53
+ const p = j.progress || {};
54
+ const prefix = p.format ? (labels[p.format] || '') + ': ' : '';
55
+ if (p.phase === 'slides') return prefix + `Preparando slide ${p.slide || p.completed + 1} de ${p.total}`;
56
+ if (p.phase === 'packing') return prefix + 'Concluindo o arquivo';
57
+ if (p.phase === 'opening') return prefix + 'Abrindo a apresentação para exportar';
58
+ if (j.status === 'queued') return 'Na fila para gerar';
59
+ return 'Preparando a exportação';
60
+ }
61
+ async function download(id) {
62
+ const row = rows.get(id); if (!row || row.downloading) return;
63
+ row.downloading = true; row.button.disabled = true; row.note.textContent = 'Recebendo o arquivo para baixar';
64
+ try {
65
+ const res = await fetch(`api/export/jobs/${id}/download`);
66
+ if (!res.ok) { let error; try { error = (await res.json()).error; } catch {} throw new Error(error || 'Não foi possível receber o arquivo.'); }
67
+ const blob = await res.blob();
68
+ const cd = res.headers.get('Content-Disposition') || '';
69
+ const name = decodeURIComponent((cd.match(/filename\*=UTF-8''([^;]+)/) || [])[1] || jobs.get(id).name);
70
+ const a = document.createElement('a'); const url = URL.createObjectURL(blob);
71
+ a.href = url; a.download = name; document.body.append(a); a.click(); a.remove();
72
+ setTimeout(() => URL.revokeObjectURL(url), 10000);
73
+ row.note.textContent = 'Download enviado ao navegador. Você pode baixar novamente.';
74
+ } catch (e) { row.note.textContent = 'Não deu para baixar: ' + e.message; }
75
+ finally { row.downloading = false; row.button.disabled = false; }
76
+ }
77
+ function render() {
78
+ const shown = [...jobs.values()].filter(j => !dismissed.has(j.id));
79
+ if (!shown.length && !panel) return;
80
+ mount(); panel.hidden = !shown.length;
81
+ panel.dataset.minimized = String(minimized);
82
+ const running = shown.filter(j => ['queued', 'running', 'starting'].includes(j.status)).length;
83
+ heading.textContent = running ? `Exportações (${running} em andamento)` : 'Exportações';
84
+ list.hidden = hint.hidden = minimized; toggle.textContent = minimized ? 'Expandir' : 'Recolher'; toggle.setAttribute('aria-expanded', String(!minimized));
85
+ for (const j of shown) {
86
+ let row = rows.get(j.id);
87
+ if (!row) {
88
+ const el = document.createElement('article'); el.dataset.exportJob = j.id;
89
+ el.innerHTML = '<strong class="export-name"></strong><p class="export-message" role="status" aria-live="polite"></p><progress aria-label="Preparação dos slides e conclusão do arquivo"></progress><small class="export-time"></small><details hidden><summary>Avisos da exportação</summary><p></p></details><div class="export-actions"><button class="export-download" type="button">Baixar arquivo</button><button class="export-dismiss" type="button">Ocultar</button></div><p class="export-note"></p>';
90
+ row = { el, name: el.querySelector('strong'), status: el.querySelector('.export-message'), bar: el.querySelector('progress'), time: el.querySelector('small'), warnings: el.querySelector('details'), button: el.querySelector('.export-download'), dismiss: el.querySelector('.export-dismiss'), note: el.querySelector('.export-note') };
91
+ row.button.onclick = () => download(j.id);
92
+ row.dismiss.onclick = () => { dismissed.add(j.id); render(); };
93
+ list.append(el); rows.set(j.id, row);
94
+ }
95
+ row.el.hidden = false; row.name.textContent = `${labels[j.kind] || 'Arquivo'} — ${j.title || j.name || 'Apresentação'}`;
96
+ const text = message(j); if (row.status.textContent !== text) row.status.textContent = text;
97
+ row.status.classList.toggle('export-error', ['failed', 'expired'].includes(j.status));
98
+ row.time.textContent = `${j.finishedAt ? 'Duração' : 'Tempo decorrido'}: ${elapsed((j.finishedAt || Date.now()) - j.createdAt)}`;
99
+ row.button.hidden = j.status !== 'ready'; row.dismiss.hidden = ['queued', 'running', 'starting'].includes(j.status);
100
+ row.bar.hidden = ['failed', 'expired'].includes(j.status);
101
+ const p = j.progress || {};
102
+ if (j.status === 'ready' || p.phase === 'slides' || p.phase === 'packing') {
103
+ row.bar.max = Math.max(1, (p.total || 0) + 1); row.bar.value = j.status === 'ready' ? row.bar.max : p.completed || 0;
104
+ } else row.bar.removeAttribute('value');
105
+ row.warnings.hidden = !j.warnings?.length; row.warnings.querySelector('p').textContent = (j.warnings || []).join('; ');
106
+ if (j.status === 'ready' && automatic.delete(j.id)) download(j.id);
107
+ }
108
+ for (const [id, row] of rows) {
109
+ if (!jobs.has(id)) { row.el.remove(); rows.delete(id); }
110
+ else if (dismissed.has(id)) row.el.hidden = true;
111
+ }
112
+ }
113
+ async function refresh() {
114
+ if (busy) return;
115
+ busy = true;
116
+ const known = new Set(jobs.keys()); // uma resposta atrasada não apaga a geração iniciada depois do pedido
117
+ try {
118
+ const { jobs: latest } = await json('api/export/jobs');
119
+ const ids = new Set(latest.map(j => j.id));
120
+ for (const j of jobs.values()) if (known.has(j.id) && !ids.has(j.id) && !j.id.startsWith('starting-')) {
121
+ j.status = 'expired'; automatic.delete(j.id);
122
+ }
123
+ for (const j of latest) jobs.set(j.id, j);
124
+ if (panel) panel.removeAttribute('data-disconnected');
125
+ for (const row of rows.values()) if (row.note.dataset.connection) { row.note.textContent = ''; delete row.note.dataset.connection; }
126
+ render();
127
+ } catch {
128
+ if (jobs.size) {
129
+ mount(); panel.dataset.disconnected = 'true';
130
+ for (const [id, row] of rows) if (['queued', 'running'].includes(jobs.get(id)?.status)) {
131
+ row.note.dataset.connection = 'true'; row.note.textContent = 'Sem conexão com o Studio. A geração pode continuar no servidor; o acompanhamento volta quando a conexão retornar.';
132
+ }
133
+ }
134
+ } finally { busy = false; }
135
+ }
136
+ window.SagadeckExports = {
137
+ async start(options, { beforeStart } = {}) {
138
+ const id = 'starting-' + crypto.randomUUID();
139
+ jobs.set(id, { id, kind: options.kind, title: options.title || 'Apresentação', status: 'starting', createdAt: Date.now() }); minimized = false; render();
140
+ try {
141
+ await beforeStart?.();
142
+ const { job } = await json('api/export/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options) });
143
+ jobs.delete(id); jobs.set(job.id, job); dismissed.delete(job.id); automatic.add(job.id); render(); refresh();
144
+ return job;
145
+ } catch (e) {
146
+ const j = jobs.get(id); j.status = 'failed'; j.error = e.message; j.finishedAt = Date.now(); render(); throw e;
147
+ }
148
+ },
149
+ };
150
+ // Espaço suficiente entre pedidos para que a página descanse; relógio não depende da resposta de rede.
151
+ setInterval(() => { if (jobs.size) render(); }, 1000);
152
+ setInterval(refresh, 1500);
153
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', refresh, { once: true }); else refresh();
154
+ })();
@@ -183,7 +183,7 @@
183
183
  </div>
184
184
  <div class="rgroup" data-adv>
185
185
  <div class="rgroup-body">
186
- <button id="btn-api-slide" class="rbtn rbtn-lg" title="Slide que executa uma requisição de verdade (tipo Postman), com o código curl e Python ao lado. Exemplos prontos: biblioteca, Nova, Exemplo: aula de APIs"><i class="ic" data-ic="plug-zap"></i><span>Slide de API</span></button>
186
+ <button id="btn-api-slide" class="rbtn rbtn-lg" title="Slide que executa uma requisição de verdade (tipo Postman), com o código curl e Python ao lado. Exemplos prontos: Demos oficiais, APIs com curl"><i class="ic" data-ic="plug-zap"></i><span>Slide de API</span></button>
187
187
  <button id="btn-api-envs" class="rbtn rbtn-lg" title="Endereços, tokens e segredos dos slides de API (ficam na sua máquina, nunca no deck)"><i class="ic" data-ic="server-cog"></i><span>Ambientes</span></button>
188
188
  <button id="btn-api-vars-studio" class="rbtn rbtn-lg" title="Abrir o painel de variáveis do deck"><i class="ic" data-ic="braces"></i><span>Variáveis</span></button>
189
189
  </div>
@@ -456,18 +456,19 @@
456
456
 
457
457
  <aside id="inspector-sidebar" class="task-pane" aria-label="Painel lateral">
458
458
  <div class="pane-header">
459
- <div class="segmented" role="tablist">
460
- <!-- como no VS Code: a aba aberta mostra o nome; as outras, só o ícone (o nome fica na dica) -->
461
- <button id="tab-btn-props" class="seg active" role="tab" title="Formatar: o conteúdo do slide"><i class="ic" data-ic="pencil"></i><span class="seg-label">Formatar</span></button>
462
- <button id="tab-btn-inspect" class="seg" role="tab" title="Propriedades do objeto selecionado, do slide e da apresentação"><i class="ic" data-ic="sliders-horizontal"></i><span class="seg-label">Propriedades</span></button>
463
- <button id="tab-btn-chat" class="seg" role="tab" title="Assistente de IA"><i class="ic" data-ic="sparkles"></i><span class="seg-label">Assistente</span><span id="chat-badge" class="chat-badge-dot"></span></button>
464
- <button id="tab-btn-vars" class="seg" role="tab" title="Variáveis do ambiente"><i class="ic" data-ic="braces"></i><span class="seg-label">Variáveis</span></button>
459
+ <div class="pane-tabs" role="tablist" aria-label="Painéis de edição e Assistente">
460
+ <div class="segmented" id="property-tabs">
461
+ <button id="tab-btn-props" class="seg" role="tab" aria-selected="false" aria-label="Formatar" title="Formatar: o conteúdo do slide"><i class="ic" data-ic="pencil"></i><span class="seg-label">Formatar</span></button>
462
+ <button id="tab-btn-inspect" class="seg" role="tab" aria-selected="false" aria-label="Propriedades" title="Propriedades do objeto selecionado, do slide e da apresentação"><i class="ic" data-ic="sliders-horizontal"></i><span class="seg-label">Propriedades</span></button>
463
+ <button id="tab-btn-vars" class="seg" role="tab" aria-selected="false" aria-label="Variáveis" title="Variáveis do ambiente"><i class="ic" data-ic="braces"></i><span class="seg-label">Variáveis</span></button>
464
+ </div>
465
+ <button id="tab-btn-chat" class="seg active" role="tab" aria-selected="true" title="Assistente de IA"><i class="ic" data-ic="sparkles"></i><span class="seg-label">Assistente</span><span id="chat-badge" class="chat-badge-dot"></span></button>
465
466
  </div>
466
467
  <button id="btn-close-pane" class="icon-btn" title="Fechar painel"><i class="ic" data-ic="x"></i></button>
467
468
  </div>
468
469
 
469
470
  <div id="tab-panel-inspect" class="tab-panel inspector-panel" role="tabpanel" aria-labelledby="tab-btn-inspect"><div id="inspector-body" class="ip-body"></div></div>
470
- <div id="tab-panel-props" class="tab-panel active">
471
+ <div id="tab-panel-props" class="tab-panel" role="tabpanel" aria-labelledby="tab-btn-props">
471
472
  <div id="auto-banner" class="auto-banner" hidden></div>
472
473
  <div id="slide-fields-form" class="fields-form"></div>
473
474
  <div class="pane-section">
@@ -476,7 +477,7 @@
476
477
  </div>
477
478
  </div>
478
479
 
479
- <div id="tab-panel-chat" class="tab-panel">
480
+ <div id="tab-panel-chat" class="tab-panel active" role="tabpanel" aria-labelledby="tab-btn-chat">
480
481
  <div class="chat-options">
481
482
  <button id="btn-brainstorm-apply" class="btn btn-primary btn-sm hidden" type="button" title="Autoriza a IA a aplicar o que vocês combinaram na conversa">Pode fazer</button>
482
483
  </div>
@@ -813,7 +814,7 @@
813
814
  </div>
814
815
  <div class="modal-dialog-body">
815
816
  <label class="field-label" for="ai-deck-briefing">Sobre o que é, para quem e com que objetivo?</label>
816
- <textarea id="ai-deck-briefing" class="form-control" rows="5" placeholder="ex.: Palestra de 20 minutos para gerentes de agência sobre usar IA com segurança no atendimento. Comece com um caso real, mostre 3 cuidados e feche com um checklist.&#10;(imagens: peça no texto — 'com fotos', 'você decide onde ilustrar'… sem pedir, não gera)"></textarea>
817
+ <textarea id="ai-deck-briefing" class="form-control" rows="5" placeholder="ex.: Palestra de 20 minutos para líderes de equipe sobre usar IA com segurança no dia a dia. Comece com um caso real, mostre 3 cuidados e feche com um checklist.&#10;(imagens: peça no texto — 'com fotos', 'você decide onde ilustrar'… sem pedir, não gera)"></textarea>
817
818
  <details id="ai-deck-advanced">
818
819
  <summary>Opções avançadas (minutos, estilo, arquivos…)</summary>
819
820
  <div class="form-row">
@@ -849,6 +850,9 @@
849
850
  <script src="ai-settings.js"></script>
850
851
  <script src="api-envs-form.js"></script>
851
852
  <script src="calc-fields.js"></script>
853
+ <script src="map-fields.js"></script>
854
+ <script src="map-settings.js"></script>
855
+ <script src="map-data.js"></script>
852
856
  <script src="art-preview.js"></script>
853
857
  <script src="api-collections.js"></script>
854
858
  <script src="developer-fields.js"></script>
@@ -858,6 +862,8 @@
858
862
  <script src="formula.js"></script>
859
863
  <script src="calc.js"></script>
860
864
  <script src="science.js"></script>
865
+ <script src="map-background.js"></script>
866
+ <script src="map.js"></script>
861
867
  <script src="diagram.js"></script>
862
868
  <script src="decision-lab.js"></script>
863
869
  <script src="visual-editor.js"></script>
@@ -878,6 +884,7 @@
878
884
  <script src="provenance.js"></script>
879
885
  <script src="header-footer.js"></script>
880
886
  <script src="preferences-ui.js"></script>
887
+ <script src="export-status.js"></script>
881
888
  <script src="app.js"></script>
882
889
  </body>
883
890
  </html>
@@ -12,7 +12,8 @@ a { color: inherit; }
12
12
  :focus-visible { outline: 3px solid var(--brand); outline-offset: 4px; }
13
13
  .ic { display: inline-flex; width: 18px; height: 18px; flex: none; vertical-align: middle; }
14
14
  .ic svg { width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
15
- body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-template-rows: 84px minmax(0, 1fr); height: 100vh; height: 100dvh; overflow: hidden; background: var(--canvas-bg); font-size: 14px; }
15
+ /* a faixa "Configure a IA" tem a linha dela (largura toda; some quando escondida); lateral e conteúdo vêm depois */
16
+ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-template-rows: 84px auto minmax(0, 1fr); height: 100vh; height: 100dvh; overflow: hidden; background: var(--canvas-bg); font-size: 14px; }
16
17
  .skip-link { position: fixed; top: -80px; left: 20px; z-index: 100; padding: 12px; background: var(--chrome); border-radius: 8px; }
17
18
  .skip-link:focus { top: 15px; }
18
19
  .lib-brand { display: flex; align-items: center; gap: 11px; padding: 0 24px; border-right: 1px solid var(--ui-line); text-decoration: none; font-size: 23px; font-weight: 720; letter-spacing: -.8px; }
@@ -28,7 +29,7 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
28
29
  .lib-search { display: flex; align-items: center; gap: 9px; width: 228px; height: 37px; color: var(--text-3); padding: 0 11px; background: transparent; border: 1px solid var(--ui-line); border-radius: 8px; }
29
30
  .lib-search input { background: none; color: var(--text); border: 0; outline: 0; width: 100%; min-width: 0; font-size: 11px; }
30
31
  .lib-search kbd { border: 1px solid var(--ui-line); border-radius: 3px; font-size: 10px; padding: 0 4px; }
31
- .ai-banner { display: flex; align-items: center; gap: 12px; margin: 10px 24px 0; padding: 10px 14px; border-radius: 10px; background: #fef3c7; color: #78350f; border: 1px solid #f59e0b; font-size: 13px; }
32
+ .ai-banner { grid-column: 1 / -1; grid-row: 2; display: flex; align-items: center; gap: 12px; margin: 10px 24px 0; padding: 10px 14px; border-radius: 10px; background: #fef3c7; color: #78350f; border: 1px solid #f59e0b; font-size: 13px; }
32
33
  .ai-banner[hidden] { display: none; }
33
34
  .ai-banner span { flex: 1; min-width: 0; overflow-wrap: anywhere; } /* o motivo pode trazer um caminho longo */
34
35
  .ai-banner button { flex: none; }
@@ -45,6 +46,7 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
45
46
  .lib-user { display: flex; align-items: center; gap: 8px; font-size: 12px; }
46
47
  .lib-avatar { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--selected); color: var(--brand); text-transform: uppercase; font-weight: 700; }
47
48
  .lib-body { display: contents; }
49
+ .lib-side, .lib-main { grid-row: 3; }
48
50
  .lib-side { min-height: 0; display: flex; flex-direction: column; padding: 14px 14px 18px; border-right: 1px solid var(--ui-line); overflow-y: auto; }
49
51
  #side { display: flex; flex-direction: column; gap: 5px; flex: 1; min-height: 360px; }
50
52
  .nav { display: flex; align-items: center; gap: 10px; min-height: 40px; flex: none; padding: 0 13px; border-radius: 8px; color: var(--text-2); width: 100%; text-align: left; background: none; border: 0; font-size: 12px; }
@@ -231,7 +233,7 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
231
233
  @media (min-width: 1680px) { .lib-main { padding: 40px 65px 70px; } .hero { padding-bottom: 48px; } .hero-stage { min-height: 320px; } .mini-dark { width: 335px; height: 207px; } .mini-dark b { font-size: 45px; } .mini-beige { left: 8%; } .mini-pop { right: 10%; } }
232
234
  @media (max-width: 1200px) { body.lib { grid-template-columns: 190px minmax(0, 1fr); } .lib-top { padding: 0 24px; gap: 9px; } .lib-brand { padding: 0 20px; } .lib-main { padding: 30px 28px 60px; } .breadcrumb { display: none; } .hero { gap: 20px; } .hero-stage { transform: scale(.87); transform-origin: center right; margin-left: -40px; } .hero h1 { font-size: 44px; } .section-head h2 { font-size: 18px; } .experience-art { padding: 18px; } .experiences-grid { gap: 17px; } }
233
235
  @media (max-width: 940px) { .hero { grid-template-columns: 1fr; padding-bottom: 30px; } .hero h1 { font-size: 45px; max-width: 480px; } .hero-stage { display: none; } .hero p { max-width: 420px; } .experiences-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .experience-art { padding: 20px; } .empty.compact { flex-wrap: wrap; } }
234
- @media (max-width: 700px) { body.lib { grid-template-columns: 1fr; grid-template-rows: 70px minmax(0, 1fr); } .lib-brand { position: absolute; left: 8px; height: 70px; padding: 0 10px; border: 0; font-size: 20px; } .lib-brand small { font-size: 6px; } .lib-logo { width: 27px; } .lib-top { padding: 0 15px 0 168px; gap: 7px; } .lib-top .spacer { display: none; } .lib-search { width: 100%; min-width: 0; padding: 0 8px; border: 0; } .lib-search kbd, #btn-new .lbl, .lib-user .uname, #btn-theme { display: none; } #btn-new { padding: 0; min-width: 35px; min-height: 35px; } .lib-body { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } .lib-side { padding: 8px 13px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--ui-line); flex-direction: row; } #side { flex-direction: row; align-items: center; min-height: 0; flex: none; gap: 4px; } .nav { min-height: 33px; width: auto; white-space: nowrap; font-size: 11px; gap: 7px; padding: 0 11px; } .nav .ic { width: 15px; height: 15px; } .nav .count { display: none; } .side-title { margin: 0 3px; font-size: 0; } .side-title button { width: 25px; height: 30px; } .lib-side .grow, .studio-note, .where, .ai-settings { display: none; } .side-foot { padding: 0; border: 0; } .lib-main { padding: 27px 22px 45px; } .hero { padding-top: 0; } .hero h1 { font-size: 41px; letter-spacing: -1.8px; } .hero p { font-size: 12px; margin: 16px 0 21px; } .eyebrow { font-size: 8px; margin-bottom: 17px; } .hero-actions { gap: 15px; } .section-head { gap: 10px; align-items: flex-start; } .section-head h2 { font-size: 17px; max-width: 230px; } .section-head .text-button { font-size: 10px; } .experiences-section { padding-top: 24px; } .experience-filters { gap: 2px; margin-bottom: 17px; } .experience-filter { padding: 7px 9px; font-size: 9px; } .experiences-grid { gap: 17px 12px; } .experience-art { padding: 13px; } .experience-art b { margin-top: 13px; } .experience-art small { letter-spacing: .8px; } .experience-meta { padding-top: 9px; } .experience-meta strong { font-size: 10px; } .experience-meta .experience-sub { font-size: 8px; line-height: 1.4; } .experience-meta .arrow { display: none; } .pop-star { font-size: 53px; } .pop-mark { border-width: 18px; } .code-cursor { width: 4px; height: 9px; left: 14px; bottom: 16px; } .code-lines { display: none; } .executive-bars { right: 11px; bottom: 13px; } .experience-art b { font-size: 10cqw; } .catalog-heading h1 { font-size: 30px; } .catalog-heading p { font-size: 12px; } .head h1 { font-size: 23px; } .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 12px; } .empty.compact { padding: 18px; gap: 12px; } .empty.compact .actions { justify-content: flex-start; } .empty.compact .empty-copy { min-width: 170px; } .over { padding: 6px; gap: 4px; opacity: 1; } .over .pill { font-size: 8px; padding: 0 6px; min-height: 25px; } .pill .ic { width: 10px; height: 10px; } .meta .info { font-size: 9px; } .meta .name { font-size: 11px; } .chip { font-size: 9px; } .ldlg { padding: 22px; } .ldlg .row .lib-btn { font-size: 11px; } }
236
+ @media (max-width: 700px) { body.lib { grid-template-columns: 1fr; grid-template-rows: 70px auto auto minmax(0, 1fr); } .lib-main { grid-row: 4; } .lib-brand { position: absolute; left: 8px; height: 70px; padding: 0 10px; border: 0; font-size: 20px; } .lib-brand small { font-size: 6px; } .lib-logo { width: 27px; } .lib-top { padding: 0 15px 0 168px; gap: 7px; } .lib-top .spacer { display: none; } .lib-search { width: 100%; min-width: 0; padding: 0 8px; border: 0; } .lib-search kbd, #btn-new .lbl, .lib-user .uname, #btn-theme { display: none; } #btn-new { padding: 0; min-width: 35px; min-height: 35px; } .lib-body { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } .lib-side { padding: 8px 13px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--ui-line); flex-direction: row; } #side { flex-direction: row; align-items: center; min-height: 0; flex: none; gap: 4px; } .nav { min-height: 33px; width: auto; white-space: nowrap; font-size: 11px; gap: 7px; padding: 0 11px; } .nav .ic { width: 15px; height: 15px; } .nav .count { display: none; } .side-title { margin: 0 3px; font-size: 0; } .side-title button { width: 25px; height: 30px; } .lib-side .grow, .studio-note, .where, .ai-settings { display: none; } .side-foot { padding: 0; border: 0; } .lib-main { padding: 27px 22px 45px; } .hero { padding-top: 0; } .hero h1 { font-size: 41px; letter-spacing: -1.8px; } .hero p { font-size: 12px; margin: 16px 0 21px; } .eyebrow { font-size: 8px; margin-bottom: 17px; } .hero-actions { gap: 15px; } .section-head { gap: 10px; align-items: flex-start; } .section-head h2 { font-size: 17px; max-width: 230px; } .section-head .text-button { font-size: 10px; } .experiences-section { padding-top: 24px; } .experience-filters { gap: 2px; margin-bottom: 17px; } .experience-filter { padding: 7px 9px; font-size: 9px; } .experiences-grid { gap: 17px 12px; } .experience-art { padding: 13px; } .experience-art b { margin-top: 13px; } .experience-art small { letter-spacing: .8px; } .experience-meta { padding-top: 9px; } .experience-meta strong { font-size: 10px; } .experience-meta .experience-sub { font-size: 8px; line-height: 1.4; } .experience-meta .arrow { display: none; } .pop-star { font-size: 53px; } .pop-mark { border-width: 18px; } .code-cursor { width: 4px; height: 9px; left: 14px; bottom: 16px; } .code-lines { display: none; } .executive-bars { right: 11px; bottom: 13px; } .experience-art b { font-size: 10cqw; } .catalog-heading h1 { font-size: 30px; } .catalog-heading p { font-size: 12px; } .head h1 { font-size: 23px; } .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 12px; } .empty.compact { padding: 18px; gap: 12px; } .empty.compact .actions { justify-content: flex-start; } .empty.compact .empty-copy { min-width: 170px; } .over { padding: 6px; gap: 4px; opacity: 1; } .over .pill { font-size: 8px; padding: 0 6px; min-height: 25px; } .pill .ic { width: 10px; height: 10px; } .meta .info { font-size: 9px; } .meta .name { font-size: 11px; } .chip { font-size: 9px; } .ldlg { padding: 22px; } .ldlg .row .lib-btn { font-size: 11px; } }
235
237
  @media (max-width: 390px) { .lib-search input { font-size: 10px; } .lib-main { padding-left: 17px; padding-right: 17px; } .hero h1 { font-size: 36px; } .lib-brand { font-size: 18px; gap: 6px; } .lib-top { padding-left: 149px; } .hero-actions { gap: 12px; } .hero-actions .lib-btn { font-size: 11px; } .section-head p { line-height: 1.6; } }
236
238
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; } }
237
239
  .lmenu.tall { max-width: 340px; } /* menu Nova: descrições quebram em vez de alargar o menu */
@@ -278,3 +280,10 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
278
280
  .dlg-ask-opts { display: flex; flex-wrap: wrap; gap: 6px; }
279
281
  .dlg-ask-free { display: flex; gap: 6px; }
280
282
  .dlg-ask-free input { flex: 1; }
283
+ /* demos oficiais (só leitura): selo com cadeado, descrição numa linha, a pasta da lateral */
284
+ .card-oficial .thumb img { width: 100%; height: 100%; object-fit: cover; }
285
+ .chip-oficial { font-weight: 600; color: var(--accent, #5b4bdb); gap: 3px; }
286
+ .chip-oficial .ic, .head h1 .ic { width: 1em; height: 1em; }
287
+ .card-oficial .meta .info { min-width: 0; }
288
+ .card-oficial .meta .info .desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
289
+ .grid-oficial { margin-top: 18px; }
@@ -33,6 +33,7 @@
33
33
  <aside class="lib-side">
34
34
  <nav id="side" aria-label="Biblioteca"></nav>
35
35
  <button type="button" class="lib-btn ghost ai-settings" id="btn-ai" hidden title="Provedores, chaves e modelos de IA"><i class="ic" data-ic="sliders-horizontal"></i><span class="lbl">Configurar IA</span></button>
36
+ <button type="button" class="lib-btn ghost ai-settings" id="btn-map" hidden title="Serviços do mapa: fundo, endereços e rotas (opcional)"><i class="ic" data-ic="map"></i><span class="lbl">Configurar mapa</span></button>
36
37
  </aside>
37
38
  <main class="lib-main" id="main" tabindex="-1" aria-live="polite"><div class="initial-loading"><span class="loading-orbit"></span>Preparando seu estúdio…</div></main>
38
39
  </div>
@@ -53,6 +54,8 @@
53
54
  <script src="ui-icons.js"></script>
54
55
  <script src="update-notice.js"></script>
55
56
  <script src="ai-settings.js"></script>
57
+ <script src="map-settings.js"></script>
58
+ <script src="export-status.js"></script>
56
59
  <script src="library.js"></script>
57
60
  </body>
58
61
  </html>
@@ -62,20 +62,40 @@
62
62
  // e, enquanto a IA não estiver configurada, a faixa "Configure a IA" (some ao salvar)
63
63
  const aiSaved = () => { $("#ai-banner").hidden = true; };
64
64
  api("api/ia").then(({ editable, configured, problem }) => {
65
- $("#btn-ai").hidden = !editable; $("#ai-banner").hidden = !editable || configured;
65
+ $("#btn-ai").hidden = !editable; $("#btn-map").hidden = !editable; $("#ai-banner").hidden = !editable || configured;
66
66
  if (problem) $("#ai-banner span").textContent = `A IA não está configurada. ${problem}`; // o arquivo existe mas não serve
67
67
  hydrate($("#ai-banner"));
68
68
  }).catch(() => {});
69
69
  $("#btn-ai").onclick = () => window.SagaAISettings.open(aiSaved);
70
+ $("#btn-map").onclick = () => window.SagaMapSettings.open(); // opcional: o mapa funciona sem configurar nada
70
71
  $("#ai-banner-btn").onclick = () => window.SagaAISettings.open(aiSaved);
71
72
  const openEditor = (id, present) => { location.href = `editor?deck=${encodeURIComponent(id)}${present ? "&present=1" : ""}`; };
72
73
 
74
+ // Demos oficiais (só leitura): as demos de um assunto só (src/studio/feature-demos.js: mapas, programação, APIs com
75
+ // curl) em primeiro, o Conheça o SagaDeck (src/studio/tour.js) e as demos de recursos da vitrine. Moram numa
76
+ // pasta própria da lateral e o Conheça aparece também entre as apresentações. Abrem em prévia (editor?model=…): navegar
77
+ // e apresentar não gravam nada; editar cria a cópia da pessoa em Modelos, como nos modelos. Sem mover, renomear nem
78
+ // excluir: a demo de fábrica nunca muda.
79
+ const officialDemos = () => GALLERY.filter(([kind]) => kind === "recurso");
80
+ const officialId = (key) => (key.startsWith("model-") ? key.slice(6) : key);
81
+ const openOfficial = (key, present) => { location.href = `editor?model=${encodeURIComponent(officialId(key))}&topic=Modelos${present ? "&present=1" : ""}`; };
82
+ function officialCard([, key, title, desc, sw]) {
83
+ return `<div class="card card-oficial" data-oficial="${esc(officialId(key))}" data-key="${esc(key)}" tabindex="0" title="Demo oficial do SagaDeck: só leitura (editar cria uma cópia sua em Modelos)">
84
+ <div class="thumb" style="background:linear-gradient(90deg,${sw.map(esc).join(",")})"><img loading="lazy" alt="" src="api/library/gallery-cover?key=${encodeURIComponent(key)}" onerror="this.remove()">
85
+ <div class="over"><button class="pill" data-present>${ic("play")}Apresentar</button><button class="pill" data-open>${ic("eye")}Abrir</button></div></div>
86
+ <div class="meta"><div class="t"><div class="name">${esc(title)}</div>
87
+ <div class="info"><span class="chip chip-oficial">${ic("lock")}demo oficial</span>·<span class="desc">${esc(desc)}</span></div></div></div>
88
+ </div>`;
89
+ }
90
+ const tourCard = () => officialCard(officialDemos().find(([, key]) => key === "model-conheca"));
91
+
73
92
  // ---------------------------------------------------------------- barra lateral
74
93
  function renderSide() {
75
94
  const loose = data.decks.filter((d) => !d.topic).length;
76
95
  $("#side").innerHTML = `
77
96
  <button class="nav ${view === "recentes" ? "active" : ""}" data-view="recentes">${ic("clock")}<span class="name">Recentes</span></button>
78
97
  <button class="nav ${view === "todas" ? "active" : ""}" data-view="todas">${ic("layout-grid")}<span class="name">Todas</span><span class="count">${data.decks.length || ""}</span></button>
98
+ <button class="nav nav-oficiais ${view === "oficiais" ? "active" : ""}" data-view="oficiais" title="Apresentações do próprio SagaDeck, só leitura: o Conheça o SagaDeck e as demos (abrem em prévia)">${ic("lock")}<span class="name">Demos oficiais</span></button>
79
99
  <div class="side-title">TÓPICOS<button title="Novo tópico" id="new-topic" aria-label="Novo tópico">${ic("plus")}</button></div>
80
100
  ${data.topics.map((t) => `<button class="nav ${view === t.id ? "active" : ""}" data-view="${esc(t.id)}" data-topic="${esc(t.id)}"><span class="dot" style="background:${esc(t.color)}"></span><span class="name">${esc(t.name)}</span><span class="count">${t.count || ""}</span></button>`).join("")}
81
101
  ${loose ? `<button class="nav ${view === "" ? "active" : ""}" data-view="" title="Arquivos .yaml deixados direto na pasta da biblioteca"><span class="dot" style="background:${NO_TOPIC.color}"></span><span class="name">${NO_TOPIC.name}</span><span class="count">${loose}</span></button>` : ""}
@@ -123,6 +143,10 @@
123
143
  (data.trash.length ? data.trash.map((t) => `<div class="trash-row"><div class="t"><b>${esc(t.title)}</b><div class="sub">Era de ${esc(t.topic || NO_TOPIC.name)} · ${t.slides || 0} slides · excluída ${ago(t.deleted)}</div></div>
124
144
  <button class="lib-btn ghost" data-restore="${esc(t.id)}">${ic("rotate-ccw")}Restaurar</button><button class="lib-btn ghost danger" data-purge="${esc(t.id)}">Excluir de vez</button></div>`).join("")
125
145
  : `<div class="empty"><div class="big">${ic("trash-2")}</div><h2>Lixeira vazia</h2></div>`);
146
+ } else if (view === "oficiais") {
147
+ const demos = officialDemos().filter(([, , title, desc]) => !q || `${title} ${desc}`.toLowerCase().includes(q));
148
+ main.innerHTML = `<div class="head"><div><h1>${ic("lock")} SagaDeck · demos oficiais</h1><div class="sub">Só leitura: abrem em prévia, sem criar arquivo. Gostou? Edite e nasce uma cópia sua em Modelos.</div></div></div>
149
+ ${demos.length ? `<div class="grid">${demos.map(officialCard).join("")}</div>` : `<div class="empty"><div class="big">${ic("search")}</div><h2>Nada encontrado</h2></div>`}`;
126
150
  } else {
127
151
  const t = view === "" ? NO_TOPIC : topicOf(view);
128
152
  const all = t ? data.decks.filter((d) => d.topic === t.id) : data.decks;
@@ -140,11 +164,13 @@
140
164
  } else if (!data.decks.length && !q) {
141
165
  main.innerHTML += `<div class="empty"><div class="big">${ic("library")}</div><h2>Sua biblioteca está vazia</h2>
142
166
  <p>Crie um tópico (ex.: Palestras, Trabalho) e comece uma apresentação — em branco, com IA ou importando um .sagadeck ou um PowerPoint (.pptx).</p>
143
- <div class="actions"><button class="lib-btn ghost" data-empty-topic>${ic("folder-plus")}Novo tópico</button><button class="lib-btn primary" data-empty-new>${ic("plus")}Nova apresentação</button></div></div>`;
167
+ <div class="actions"><button class="lib-btn ghost" data-empty-topic>${ic("folder-plus")}Novo tópico</button><button class="lib-btn primary" data-empty-new>${ic("plus")}Nova apresentação</button></div></div>
168
+ <div class="grid grid-oficial">${tourCard()}</div>`;
144
169
  } else if (!list.length) {
145
170
  main.innerHTML += `<div class="empty"><div class="big">${ic("search")}</div><h2>Nada encontrado</h2><p>Nenhuma apresentação com "${esc(q)}".</p></div>`;
146
171
  } else {
147
- main.innerHTML += `<div class="grid">${t && !q ? `<div class="card new-card" id="card-new" tabindex="0"><div class="thumb"><div class="plus">${ic("plus")}Nova em ${esc(t.name)}</div></div></div>` : ""}${items.map((d) => card(d, !t)).join("")}</div>`;
172
+ // em Recentes e Todas, o Conheça o SagaDeck (demo oficial) vem primeiro, para quem ainda não abriu a pasta
173
+ main.innerHTML += `<div class="grid">${t && !q ? `<div class="card new-card" id="card-new" tabindex="0"><div class="thumb"><div class="plus">${ic("plus")}Nova em ${esc(t.name)}</div></div></div>` : ""}${!t && !q ? tourCard() : ""}${items.map((d) => card(d, !t)).join("")}</div>`;
148
174
  }
149
175
  const sortSel = $("#sort");
150
176
  if (sortSel) { sortSel.value = sortBy; sortSel.onchange = () => { sortBy = sortSel.value; store.set("libSort", sortBy); render(); }; }
@@ -155,6 +181,10 @@
155
181
  }
156
182
 
157
183
  function wire(main) {
184
+ main.querySelectorAll(".card-oficial").forEach((c) => {
185
+ c.onclick = (e) => openOfficial(c.dataset.key, !!e.target.closest("[data-present]"));
186
+ c.onkeydown = (e) => { if (e.key === "Enter") openOfficial(c.dataset.key); };
187
+ });
158
188
  main.querySelectorAll(".card[data-id]").forEach((c) => {
159
189
  const id = c.dataset.id;
160
190
  c.ondragstart = (e) => { e.dataTransfer.setData("application/x-sagadeck-deck", id); c.classList.add("dragging"); };
@@ -224,8 +254,7 @@
224
254
  m.querySelectorAll("[data-move]").forEach((b) => b.onclick = () => { closeMenus(); act(() => api("api/library/decks/move", { id, topic: b.dataset.move }), `Movida para ${topicOf(b.dataset.move).name}`); });
225
255
  m.querySelectorAll("[data-dl]").forEach((b) => b.onclick = () => {
226
256
  closeMenus();
227
- if (b.dataset.dl !== "sagadeck") toast("Gerando… o download começa em alguns segundos.", 6000);
228
- location.href = `api/library/download?id=${encodeURIComponent(id)}&kind=${b.dataset.dl}${b.dataset.notas === "0" ? "&notas=0" : ""}`;
257
+ window.SagadeckExports.start({ id, kind: b.dataset.dl, notes: b.dataset.notas !== '0', title: d.title }).catch(err => toast('Não deu para gerar: ' + err.message, 8000));
229
258
  });
230
259
  }
231
260
 
@@ -277,15 +306,13 @@
277
306
  ["visual", "model-lavanda", "Estúdio lavanda", "Mosaicos, cápsulas e composições editoriais", ["#EFE9FB", "#5B3FA8", "#D8C8F5"]],
278
307
  ["visual", "model-executivo", "Relatório executivo", "Resumo, indicadores e plano de trabalho", ["#0F1115", "#E4B660", "#EDEBE6"]],
279
308
  ["visual", "model-workshop", "Workshop visual", "Perguntas, código guiado e consulta", ["#FFF7E6", "#1A1A1A", "#E8590C"]],
280
- ["recurso", "model-explorar", "Explore uma ideia", "Prever, comparar cenários, curvas conectadas e acompanhar o mesmo objeto entre cenas", ["#F4F1EA", "#1B1F3B", "#E8590C"]],
281
- ["recurso", "model-novidades", "Novidades", "Carrossel, gráfico do Excel e da planilha do projeto, fórmulas vivas, duelo de commits, terminais e zoom", ["#F4F1EA", "#1B1F3B", "#E8590C"]],
309
+ ["recurso", "model-mapas", "Mapas", "Pontos, bolhas, regiões, linhas e áreas; rota, alcance, raio e faixa calculados; a IA e o Editar mapa", ["#EAF4F8", "#0B2A3C", "#0E7CB8"]],
310
+ ["recurso", "model-programacao", "Programação", "Código destacado e guiado, código que a plateia roda, algoritmo animado, Git a dois e diagramas", ["#0C0F0B", "#F0B24A", "#A9C79B"]],
311
+ ["recurso", "model-apis", "APIs com curl", "A anatomia de um curl, chamadas ao vivo com o curl para copiar, polling, streaming e as opções do dia a dia", ["#170A10", "#F2506A", "#F3A6B5"]],
312
+ ["recurso", "model-conheca", "Conheça o SagaDeck", "Um slide por recurso, com a frase para pedir cada um à IA", ["#F2F0EA", "#1A1A1A", "#F2B33D"]],
282
313
  ["recurso", "model-hidraulica", "Aula de hidráulica", "Navier–Stokes, curva que se mexe, calculadora de Reynolds ao vivo e exercício resolvido passo a passo", ["#EAF4F8", "#0B5C7A", "#1B998B"]],
283
314
  ["recurso", "model-algoritmos", "Aula de algoritmos", "Ordenação e busca rodando de verdade: barras, código, contadores e o botão Tocar", ["#0F1426", "#7C8CFF", "#F2B33D"]],
284
- ["recurso", "model-compacto", "Material de consulta", "Código completo, JSON e páginas compactas", ["#F7F6F2", "#20242B", "#0F6CBD"]],
285
- ["recurso", "model-avancado", "Recursos avançados", "Conteúdo denso, avisos, diagramas e tipografia cinética", ["#F7F6F2", "#20242B", "#E8590C"]],
286
315
  ["recurso", "model-diagramas", "Diagramas vivos", "Fluxo, sequência, estados e mapa mental", ["#F4F7FB", "#1F6FB2", "#5DBB86"]],
287
- ["recurso", "example-api", "Aula de APIs ao vivo", "Slides que executam requisições de verdade. Roda sem configurar nada", ["#111B2B", "#5B8DEF", "#ECF1FF"]],
288
- ["recurso", "example-cenario", "Texto no cenário", "As 13 composições: fundo, transparência, recorte na frente das letras", ["#0A0D17", "#00F2FE", "#FF007A"]],
289
316
  ];
290
317
  const GALLERY_KINDS = [["", "Todos"], ["visual", "Visuais para começar"], ["recurso", "Recursos do SagaDeck"]];
291
318
  async function galleryDialog(topic, kind = "") {
@@ -365,7 +392,7 @@
365
392
  let reading = 0;
366
393
  dialog(`<h3 id="dialog-heading">Nova apresentação com IA</h3>
367
394
  <label for="dlg-brief">Sobre o que é, para quem e com que objetivo? <span class="muted">Ou só anexe o material abaixo.</span></label>
368
- <textarea id="dlg-brief" placeholder="Ex.: palestra para gestores sobre golpes no Pix, tom leve, começando com um caso real. Ou: resumo deste relatório para a diretoria, focando nos números do ano."></textarea>
395
+ <textarea id="dlg-brief" placeholder="Ex.: palestra para gestores sobre trabalho híbrido, tom leve, começando com um caso real. Ou: resumo deste relatório para a diretoria, focando nos números do ano."></textarea>
369
396
  <div class="dlg-two">
370
397
  <div><label for="dlg-min">Quanto tempo você tem?</label><div class="dlg-min"><input type="number" id="dlg-min" min="1" max="120" value="10"><span>min</span><span class="hint" id="dlg-slides"></span></div></div>
371
398
  <div><label for="dlg-style">Estilo</label><select id="dlg-style"><option value="">Automático (a IA escolhe pelo assunto)</option>${STYLES.map(([v, l]) => `<option value="${v}">${esc(l)}</option>`).join("")}</select></div>