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.
- package/README.md +55 -4
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +483 -1
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +184 -105
- package/package.json +34 -2
- package/src/ai/commands.js +2 -2
- package/src/ai/deck-ai.js +14 -5
- package/src/ai/ia-check.js +127 -0
- package/src/ai/ia-config.js +2 -2
- package/src/ai/llm.js +5 -1
- package/src/ai/map-analysis-run.js +119 -0
- package/src/ai/map-data.js +221 -0
- package/src/ai/quality.js +1 -1
- package/src/ai/video-generation.js +70 -25
- package/src/build.js +51 -15
- package/src/elements.js +39 -1
- package/src/export/browser.js +23 -1
- package/src/export/pptx.js +9 -3
- package/src/export/shots.js +10 -3
- package/src/fiscal/normalize.js +19 -0
- package/src/fs-retry.js +37 -2
- package/src/import/office.js +5 -4
- package/src/layouts.js +114 -99
- package/src/lessons.js +1 -1
- package/src/library.js +7 -7
- package/src/map-analysis.js +109 -0
- package/src/map-config.js +89 -0
- package/src/map-routing.js +82 -0
- package/src/map-services.js +60 -0
- package/src/map.js +274 -0
- package/src/mcp/server.js +17 -0
- package/src/motion.js +20 -6
- package/src/purpose.js +1 -1
- package/src/runtime/base.css +105 -329
- package/src/runtime/fit.js +3 -2
- package/src/runtime/fonts/amatic-sc.css +3 -0
- package/src/runtime/fonts/bebas-neue.css +2 -0
- package/src/runtime/fonts/cabin-sketch.css +2 -0
- package/src/runtime/fonts/fraunces.css +3 -0
- package/src/runtime/fonts/gelasio.css +3 -0
- package/src/runtime/fonts/ibm-plex-mono.css +4 -0
- package/src/runtime/fonts/index.json +10 -1
- package/src/runtime/fonts/itim.css +2 -0
- package/src/runtime/fonts/montserrat.css +2 -0
- package/src/runtime/fonts/space-grotesk.css +2 -0
- package/src/runtime/map-background.js +91 -0
- package/src/runtime/map.js +266 -0
- package/src/runtime/runtime.js +81 -100
- package/src/runtime/skins/caderno.css +33 -0
- package/src/runtime/skins/cartaz.css +27 -0
- package/src/runtime/skins/infografico.css +29 -0
- package/src/runtime/skins/lousa.css +22 -0
- package/src/runtime/skins/musgo.css +42 -0
- package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
- package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
- package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
- package/src/runtime/vendor/geo-contornos.json +1 -0
- package/src/runtime/vendor/leaflet.css +661 -0
- package/src/runtime/vendor/leaflet.js +6 -0
- package/src/runtime/vendor/maplibre-gl.css +1 -0
- package/src/runtime/vendor/maplibre-gl.js +825 -0
- package/src/runtime/vendor/maplibre-leaflet.js +234 -0
- package/src/runtime/vendor/maplibre-worker.js +10 -0
- package/src/studio/ai-chat-route.js +20 -0
- package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
- package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
- package/src/studio/command-approval.js +2 -3
- package/src/studio/demo-decks.js +6 -2
- package/src/studio/export-jobs.js +86 -0
- package/src/studio/exporting.js +36 -18
- package/src/studio/feature-demos.js +144 -0
- package/src/studio/geo-routes.js +138 -0
- package/src/studio/ia-routes.js +9 -0
- package/src/studio/layout-samples.js +78 -16
- package/src/studio/library-routes.js +0 -9
- package/src/studio/project.js +2 -1
- package/src/studio/public/ai-settings.js +6 -3
- package/src/studio/public/app.js +46 -52
- package/src/studio/public/explorer.js +23 -8
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/index.html +17 -10
- package/src/studio/public/library.css +12 -3
- package/src/studio/public/library.html +3 -0
- package/src/studio/public/library.js +39 -12
- package/src/studio/public/map-data.js +158 -0
- package/src/studio/public/map-editor.js +676 -0
- package/src/studio/public/map-fields.js +21 -0
- package/src/studio/public/map-settings.js +86 -0
- package/src/studio/public/slide-form.js +19 -16
- package/src/studio/public/style.css +58 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/public/viewers.js +2 -1
- package/src/studio/public/visual-editor.js +1 -1
- package/src/studio/server.js +34 -9
- package/src/studio/snapshot.js +8 -4
- package/src/studio/template-collections.js +1 -1
- package/src/studio/tour.js +158 -0
- package/src/themes.js +154 -0
- package/templates/cenario-e-ciencia.yaml +1 -21
- package/templates/ensaio-api.yaml +3 -3
- package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
- package/templates/cenario/imagens/cidade.svg +0 -30
- 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:
|
|
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="
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
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
|
|
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. (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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
location.href = `api/library/download?id=${encodeURIComponent(id)}&kind=${b.dataset.dl}${b.dataset.notas === "0" ? "¬as=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-
|
|
281
|
-
["recurso", "model-
|
|
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
|
|
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>
|