sagadeck 1.8.0 → 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 +19 -8
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +367 -7
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +131 -110
- package/package.json +33 -2
- package/src/ai/commands.js +2 -2
- package/src/ai/deck-ai.js +1 -1
- 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 +1 -1
- package/src/ai/quality.js +1 -1
- package/src/ai/video-generation.js +70 -25
- package/src/build.js +46 -14
- package/src/elements.js +39 -1
- package/src/export/pptx.js +7 -2
- package/src/export/shots.js +7 -2
- 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 +104 -99
- package/src/lessons.js +1 -1
- package/src/library.js +7 -7
- package/src/map-analysis.js +4 -1
- package/src/map-config.js +10 -4
- package/src/map.js +21 -8
- package/src/mcp/server.js +17 -0
- package/src/motion.js +20 -6
- package/src/purpose.js +1 -1
- package/src/runtime/base.css +88 -332
- 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 +41 -9
- 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/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
- package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
- package/src/runtime/vendor/geo-contornos.json +1 -1
- 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/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 +4 -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/ia-routes.js +9 -0
- package/src/studio/layout-samples.js +9 -18
- package/src/studio/library-routes.js +0 -9
- package/src/studio/project.js +1 -0
- package/src/studio/public/ai-settings.js +2 -2
- package/src/studio/public/app.js +41 -51
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/index.html +13 -10
- package/src/studio/public/library.css +12 -3
- package/src/studio/public/library.html +1 -0
- package/src/studio/public/library.js +36 -14
- package/src/studio/public/map-data.js +7 -3
- package/src/studio/public/map-editor.js +625 -236
- package/src/studio/public/map-settings.js +3 -1
- package/src/studio/public/slide-form.js +18 -15
- package/src/studio/public/style.css +54 -9
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +27 -12
- package/src/studio/snapshot.js +8 -4
- package/src/studio/template-collections.js +1 -1
- package/src/studio/tour.js +89 -7
- 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
package/src/studio/public/app.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
const LAYOUT_NAMES = [
|
|
36
36
|
"cover", "section", "statement", "definition", "poster", "calendar", "headline", "quote", "number", "split", "full",
|
|
37
37
|
"cards", "bento", "mosaic", "ribbon", "stats", "steps", "funnel", "pyramid", "list", "agenda", "timeline", "hub", "status", "onepage",
|
|
38
|
-
"chart", "table", "map", "compare", "matrix", "decisionlab", "diagram", "infographic", "question", "poll", "image", "code", "codewalk", "spotlight", "
|
|
38
|
+
"chart", "table", "map", "compare", "matrix", "decisionlab", "diagram", "infographic", "question", "poll", "image", "code", "codewalk", "spotlight", "portal", "science", "video", "carousel", "duel", "terminals", "turns", "solution", "calc", "algo", "graphlab", "codelab", "playground",
|
|
39
39
|
"blocks", "dossier", "canvas", "references", "end",
|
|
40
40
|
];
|
|
41
41
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
split: "Texto e figura", cards: "Cartões", stats: "Indicadores", steps: "Etapas", list: "Lista",
|
|
46
46
|
timeline: "Linha do tempo", chart: "Gráfico de dados", table: "Tabela", map: "Mapa", compare: "Comparação", matrix: "Matriz 2×2",
|
|
47
47
|
question: "Pergunta", poll: "Enquete", image: "Imagem", code: "Código", video: "Vídeo",
|
|
48
|
-
diagram: "Diagrama", infographic: "Infográfico", mosaic: "Grade adaptável", ribbon: "Cápsulas", dossier: "Página de consulta", decisionlab: "Laboratório de decisões", science: "Fórmulas e funções",
|
|
48
|
+
diagram: "Diagrama", infographic: "Infográfico", mosaic: "Grade adaptável", ribbon: "Cápsulas", dossier: "Página de consulta", decisionlab: "Laboratório de decisões", science: "Fórmulas e funções", codewalk: "Código guiado", spotlight: "Foco guiado",
|
|
49
49
|
duel: "Duelo de commits", terminals: "Dois terminais", turns: "Turnos a dois", carousel: "Carrossel", solution: "Exercício resolvido", calc: "Calculadora ao vivo", algo: "Algoritmo animado",
|
|
50
50
|
graphlab: "Rede explorável", codelab: "Código explorável", playground: "Experiência personalizada", portal: "Portal e transformação",
|
|
51
51
|
blocks: "Livre (blocos)", canvas: "Livre (posições)", end: "Encerramento", references: "Referências",
|
|
@@ -429,7 +429,7 @@
|
|
|
429
429
|
dom.renderedSlideContainer.innerHTML = data.html;
|
|
430
430
|
window.SagaScience?.mount(dom.renderedSlideContainer);
|
|
431
431
|
window.SagaMap?.mount(dom.renderedSlideContainer); // só aqui (prévia grande); miniaturas e galeria ficam na prévia em SVG
|
|
432
|
-
window.SagaMapData?.attach(dom.renderedSlideContainer, slide, () => { syncDeckToServer(); renderCurrentSlide(); }, { toast: showToast, hydrate: hydrateIcons });
|
|
432
|
+
window.SagaMapData?.attach(dom.renderedSlideContainer, slide, () => { syncDeckToServer(); renderCurrentSlide(); }, { toast: showToast, hydrate: hydrateIcons, materialized: previewMaterialized });
|
|
433
433
|
window.SagaCalc?.mount(dom.renderedSlideContainer); // calculadora ao vivo: os controles funcionam no editor também
|
|
434
434
|
window.SagaDiagrams?.mount(dom.renderedSlideContainer).then(() => reportDiagram(dom.renderedSlideContainer));
|
|
435
435
|
window.SagaDecisionLab?.mount(dom.renderedSlideContainer);
|
|
@@ -2053,7 +2053,7 @@
|
|
|
2053
2053
|
["processo", "Processos e diagramas", ["steps", "timeline", "funnel", "pyramid", "diagram", "infographic", "hub"]],
|
|
2054
2054
|
["gestao", "Gestão e status", ["status", "onepage", "decisionlab"]],
|
|
2055
2055
|
["codigo", "Código e API", ["code", "codewalk", "api"]],
|
|
2056
|
-
["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "
|
|
2056
|
+
["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "portal"]],
|
|
2057
2057
|
["plateia", "Plateia", ["question", "poll"]],
|
|
2058
2058
|
["dinamicas", "Dinâmicas a dois", ["duel", "terminals", "turns"]],
|
|
2059
2059
|
["aula", "Aula: exercícios e algoritmos", ["solution", "calc", "algo", "graphlab", "codelab", "playground"]],
|
|
@@ -2329,38 +2329,13 @@
|
|
|
2329
2329
|
|
|
2330
2330
|
// Ao trocar de layout, o conteúdo em lista (cartões, etapas, eventos, níveis…) vai junto
|
|
2331
2331
|
const LIST_KEY = { cards: "items", bento: "tiles", stats: "stats", steps: "steps", funnel: "stages", pyramid: "levels", list: "items",
|
|
2332
|
-
agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts"
|
|
2332
|
+
agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts" };
|
|
2333
2333
|
const SOURCE_KEYS = ["items", "tiles", "stats", "kpis", "steps", "process", "flow", "stages", "levels", "events", "cells", "options", "bullets", "lines", "contacts"];
|
|
2334
|
-
function kineticPhrases(value) {
|
|
2335
|
-
const words = String(value || "").match(/==[^=]+==|\*\*.*?\*\*|\*[^*]+\*|~~.*?~~|`[^`]+`|\[[^\]]+\]\([^)]+\)|\S+/g) || [];
|
|
2336
|
-
const connectors = new Set(["a", "as", "de", "do", "da", "dos", "das", "e", "em", "no", "na", "nos", "nas", "para", "por", "com", "o", "os"]);
|
|
2337
|
-
const phrases = [];
|
|
2338
|
-
while (words.length) {
|
|
2339
|
-
let take = Math.ceil(words.length / Math.ceil(words.length / 4));
|
|
2340
|
-
const last = words[take - 1]?.replace(/^[^A-Za-zÀ-ÿ]+|[^A-Za-zÀ-ÿ]+$/g, "").toLowerCase();
|
|
2341
|
-
if (take > 1 && connectors.has(last)) take--;
|
|
2342
|
-
phrases.push(words.splice(0, take).join(" "));
|
|
2343
|
-
}
|
|
2344
|
-
return phrases;
|
|
2345
|
-
}
|
|
2346
2334
|
function carryContent(slide, to) {
|
|
2347
2335
|
const target = LIST_KEY[to];
|
|
2348
2336
|
if (!target) return;
|
|
2349
2337
|
const activeKey = LIST_KEY[slide.layout];
|
|
2350
2338
|
if (activeKey === target && Array.isArray(slide[target]) && slide[target].length) return;
|
|
2351
|
-
if (to === "kinetic") {
|
|
2352
|
-
const source = SOURCE_KEYS.find((key) => Array.isArray(slide[key]) && slide[key].length);
|
|
2353
|
-
if (source) {
|
|
2354
|
-
slide.beats = slide[source].map((item) => {
|
|
2355
|
-
if (typeof item !== "object" || item == null) return { text: String(item ?? "") };
|
|
2356
|
-
return { text: item.title ?? item.label ?? item.text ?? item.name ?? "", ...(item.text && item.title ? { tag: item.text } : {}) };
|
|
2357
|
-
});
|
|
2358
|
-
} else {
|
|
2359
|
-
const phrases = [...kineticPhrases(slide.title || slide.text), ...kineticPhrases(slide.subtitle)];
|
|
2360
|
-
if (phrases.length) slide.beats = phrases.map((text, i) => ({ text, ...(i === 0 && slide.kicker ? { tag: slide.kicker } : {}) }));
|
|
2361
|
-
}
|
|
2362
|
-
return;
|
|
2363
|
-
}
|
|
2364
2339
|
const srcKey = [activeKey, ...SOURCE_KEYS].find((k) => k && Array.isArray(slide[k]) && slide[k].length);
|
|
2365
2340
|
if (!srcKey) return;
|
|
2366
2341
|
const entries = slide[srcKey].map((x) => {
|
|
@@ -2391,7 +2366,7 @@
|
|
|
2391
2366
|
}
|
|
2392
2367
|
if (layoutName === "quote") slide.quote = primary;
|
|
2393
2368
|
if (["question", "poll"].includes(layoutName)) slide.question = primary;
|
|
2394
|
-
if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length &&
|
|
2369
|
+
if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length && layoutName !== "statement") {
|
|
2395
2370
|
const content = [slide.subtitle, slide.body, primary].find(v => typeof v === "string" && v.trim());
|
|
2396
2371
|
if (content) slide[LIST_KEY[layoutName]] = content.split(/\n+/).filter(Boolean).map(text => ["list", "split", "question", "poll", "references", "end"].includes(layoutName) ? text : {title:text});
|
|
2397
2372
|
}
|
|
@@ -2452,6 +2427,23 @@
|
|
|
2452
2427
|
} catch (e) { showToast(`Não deu para atualizar: ${e.message}`); }
|
|
2453
2428
|
},
|
|
2454
2429
|
hydrate: hydrateIcons,
|
|
2430
|
+
splitCalendar: async () => {
|
|
2431
|
+
const at = state.currentSlideIndex, slide = state.deck.slides[at];
|
|
2432
|
+
const { calendarMonth, calendarMonths } = window.SagaCalendarMonths;
|
|
2433
|
+
const MESES = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
|
|
2434
|
+
const months = calendarMonths(slide);
|
|
2435
|
+
if (months.length < 2) return;
|
|
2436
|
+
const { uid, events, undated, month, week, ...base } = slide;
|
|
2437
|
+
const slides = months.map((m, k) => ({ ...base, ...(k === 0 && uid ? { uid } : {}), view: "mes", month: m,
|
|
2438
|
+
kicker: base.kicker || `${MESES[+m.slice(5) - 1]} ${m.slice(0, 4)}`,
|
|
2439
|
+
events: events.filter((e) => calendarMonth(e?.date) === m),
|
|
2440
|
+
...(k === 0 && (undated?.length || events.some((e) => !calendarMonth(e?.date))) ? { undated: [...(undated || []), ...events.filter((e) => !calendarMonth(e?.date)).map(({ title, text }) => ({ title, ...(text ? { text } : {}) }))] } : {}) }));
|
|
2441
|
+
state.deck.slides.splice(at, 1, ...slides);
|
|
2442
|
+
await syncDeckToServer();
|
|
2443
|
+
renderThumbnails();
|
|
2444
|
+
await renderCurrentSlide();
|
|
2445
|
+
showToast(`Um slide por mês: ${slides.length} slides, na visão de mês.`, 4000);
|
|
2446
|
+
},
|
|
2455
2447
|
pickIcon: (cb) => openIconPicker(null, cb),
|
|
2456
2448
|
layoutLabel,
|
|
2457
2449
|
generateImage: (element, btn) => {
|
|
@@ -2609,7 +2601,9 @@
|
|
|
2609
2601
|
}
|
|
2610
2602
|
|
|
2611
2603
|
async function startPresentation({ pen = false } = {}) {
|
|
2612
|
-
|
|
2604
|
+
// a apresentação é montada a partir do deck do servidor. Modelo em prévia: o servidor já tem o mesmo deck (qualquer
|
|
2605
|
+
// mudança teria criado a cópia e saído da prévia), e enviar de novo contaria como mudança: apresentar não grava nada
|
|
2606
|
+
if (!state.preview) await syncDeckToServer();
|
|
2613
2607
|
const start = state.currentSlideIndex + 1;
|
|
2614
2608
|
dom.presModal.classList.remove("hidden");
|
|
2615
2609
|
dom.presFrame.src = scopedURL(`preview?t=${Date.now()}#${start}`);
|
|
@@ -2848,7 +2842,7 @@
|
|
|
2848
2842
|
// outra aba/aparelho abriu outra apresentação neste Studio: nada desta foi gravado (o servidor recusou)
|
|
2849
2843
|
function staleTab(msg) { updateSaveStatus("error"); showToast(msg, 20000, { label: "Recarregar", fn: () => location.reload() }); }
|
|
2850
2844
|
|
|
2851
|
-
async function syncDeckToServer() {
|
|
2845
|
+
async function syncDeckToServer({ throwOnError = false } = {}) {
|
|
2852
2846
|
trackDeck("Edição");
|
|
2853
2847
|
updateSaveStatus("saving");
|
|
2854
2848
|
try {
|
|
@@ -2858,7 +2852,7 @@
|
|
|
2858
2852
|
body: JSON.stringify({ spec: state.deck, expectFile: state.file }),
|
|
2859
2853
|
});
|
|
2860
2854
|
const data = await res.json().catch(() => ({}));
|
|
2861
|
-
if (data.stale)
|
|
2855
|
+
if (data.stale) { staleTab(data.error); if (throwOnError) throw new Error(data.error); return; }
|
|
2862
2856
|
if ("file" in data) state.file = data.file;
|
|
2863
2857
|
if (data.materialized) previewMaterialized(data.materialized);
|
|
2864
2858
|
// o servidor dá identidade a slide novo (uid) e leva o ajuste visual junto quando um texto muda: o Studio adota
|
|
@@ -2872,9 +2866,11 @@
|
|
|
2872
2866
|
if (adopted && !state.applyingHistory) deckHistory.rebase(state.deck);
|
|
2873
2867
|
updateSaveStatus(res.ok ? "saved" : "error");
|
|
2874
2868
|
if (res.ok) window.SagaProject?.deckChanged();
|
|
2869
|
+
else if (throwOnError) throw new Error(data.error || 'Não foi possível salvar as mudanças antes de exportar.');
|
|
2875
2870
|
} catch (err) {
|
|
2876
2871
|
console.warn("Erro ao sincronizar com servidor:", err);
|
|
2877
2872
|
updateSaveStatus("error");
|
|
2873
|
+
if (throwOnError) throw err;
|
|
2878
2874
|
}
|
|
2879
2875
|
}
|
|
2880
2876
|
|
|
@@ -3635,9 +3631,8 @@ ${ta.value}`;
|
|
|
3635
3631
|
|
|
3636
3632
|
const isMobile = () => window.matchMedia("(max-width: 900px)").matches;
|
|
3637
3633
|
|
|
3638
|
-
//
|
|
3639
|
-
|
|
3640
|
-
const PANE_TABS = ["props", "inspect", "chat", "vars"];
|
|
3634
|
+
// Formatar, Propriedades e Variáveis ficam agrupados; o Assistente tem acesso fixo separado, por último.
|
|
3635
|
+
const PANE_TABS = ["props", "inspect", "vars", "chat"];
|
|
3641
3636
|
const paneEls = (w) => ({ btn: document.getElementById(`tab-btn-${w}`), panel: document.getElementById(`tab-panel-${w}`) });
|
|
3642
3637
|
const currentPane = () => PANE_TABS.find((w) => paneEls(w).panel?.classList.contains("active")) || "props";
|
|
3643
3638
|
// menu de botão direito genérico (itens: { label, ic, fn, danger, disabled } ou { sep: true })
|
|
@@ -3665,7 +3660,7 @@ ${ta.value}`;
|
|
|
3665
3660
|
const fab = document.getElementById("chat-fab");
|
|
3666
3661
|
if (!fab) return;
|
|
3667
3662
|
const visible = isMobile() ? dom.inspectorSidebar.classList.contains("mobile-open") : !dom.inspectorSidebar.classList.contains("collapsed");
|
|
3668
|
-
fab.classList.toggle("hidden", visible
|
|
3663
|
+
fab.classList.toggle("hidden", visible); // com o painel aberto, o botão Assistente já está sempre à vista
|
|
3669
3664
|
const badge = document.getElementById("chat-badge");
|
|
3670
3665
|
if (visible && currentPane() === "chat" && badge?.classList.contains("show")) badge.classList.remove("show"); // só se precisar: o observador do selo chama esta função
|
|
3671
3666
|
fab.classList.toggle("has-news", !!document.getElementById("chat-badge")?.classList.contains("show"));
|
|
@@ -4314,29 +4309,24 @@ ${ta.value}`;
|
|
|
4314
4309
|
return { res, name };
|
|
4315
4310
|
}
|
|
4316
4311
|
|
|
4317
|
-
// PowerPoint / PDF / roteiro:
|
|
4312
|
+
// PowerPoint / PDF / roteiro: acompanham a fila do servidor, sem prender a página no download.
|
|
4318
4313
|
const EXPORTS = {
|
|
4319
|
-
pptx:
|
|
4320
|
-
|
|
4321
|
-
roteiro: { label: "o roteiro", done: "roteiro com miniaturas, notas e tempos." },
|
|
4322
|
-
estudo: { label: "o material de estudo", done: "material de estudo: cada slide inteiro e o texto de consulta." },
|
|
4323
|
-
"estudo-html": { label: "o material de estudo", done: "material de estudo num HTML só." },
|
|
4324
|
-
tudo: { label: "o PowerPoint, o PDF e o roteiro", done: "um .zip com o PowerPoint (com notas), o PDF e o roteiro." },
|
|
4314
|
+
pptx: "o PowerPoint", pdf: "o PDF", roteiro: "o roteiro", estudo: "o material de estudo",
|
|
4315
|
+
"estudo-html": "o material de estudo", tudo: "o PowerPoint, o PDF e o roteiro",
|
|
4325
4316
|
};
|
|
4326
4317
|
document.querySelectorAll("[data-export]").forEach((btn) => {
|
|
4327
4318
|
btn.onclick = async (e) => {
|
|
4328
4319
|
e.preventDefault();
|
|
4329
4320
|
if (btn.disabled) return;
|
|
4330
4321
|
const kind = btn.dataset.export, clean = btn.dataset.notas === "0";
|
|
4331
|
-
const
|
|
4322
|
+
const label = clean ? "o PowerPoint sem as notas" : EXPORTS[kind];
|
|
4332
4323
|
btn.disabled = true;
|
|
4333
|
-
showToast(`Gerando ${x.label} (${state.deck.slides.length} slides)… pode levar alguns segundos.`, 60000);
|
|
4334
4324
|
try {
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4325
|
+
await window.SagadeckExports.start({ kind, notes: !clean, title: state.deck.title }, { beforeStart: async () => {
|
|
4326
|
+
if (!state.preview) await syncDeckToServer({ throwOnError: true });
|
|
4327
|
+
} });
|
|
4338
4328
|
} catch (err) {
|
|
4339
|
-
showToast(`Não deu para gerar ${
|
|
4329
|
+
showToast(`Não deu para gerar ${label}: ${err.message}`, 8000);
|
|
4340
4330
|
} finally {
|
|
4341
4331
|
btn.disabled = false;
|
|
4342
4332
|
}
|
|
@@ -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">
|
|
@@ -861,6 +862,7 @@
|
|
|
861
862
|
<script src="formula.js"></script>
|
|
862
863
|
<script src="calc.js"></script>
|
|
863
864
|
<script src="science.js"></script>
|
|
865
|
+
<script src="map-background.js"></script>
|
|
864
866
|
<script src="map.js"></script>
|
|
865
867
|
<script src="diagram.js"></script>
|
|
866
868
|
<script src="decision-lab.js"></script>
|
|
@@ -882,6 +884,7 @@
|
|
|
882
884
|
<script src="provenance.js"></script>
|
|
883
885
|
<script src="header-footer.js"></script>
|
|
884
886
|
<script src="preferences-ui.js"></script>
|
|
887
|
+
<script src="export-status.js"></script>
|
|
885
888
|
<script src="app.js"></script>
|
|
886
889
|
</body>
|
|
887
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; }
|