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.
Files changed (92) hide show
  1. package/README.md +19 -8
  2. package/SKILL.md +10 -3
  3. package/bin/sagadeck.js +23 -0
  4. package/docs/ANDAMENTO.md +367 -7
  5. package/docs/PLANO-MELHORIAS.md +180 -0
  6. package/docs/REFERENCIA.md +131 -110
  7. package/package.json +33 -2
  8. package/src/ai/commands.js +2 -2
  9. package/src/ai/deck-ai.js +1 -1
  10. package/src/ai/ia-check.js +127 -0
  11. package/src/ai/ia-config.js +2 -2
  12. package/src/ai/llm.js +5 -1
  13. package/src/ai/map-analysis-run.js +1 -1
  14. package/src/ai/quality.js +1 -1
  15. package/src/ai/video-generation.js +70 -25
  16. package/src/build.js +46 -14
  17. package/src/elements.js +39 -1
  18. package/src/export/pptx.js +7 -2
  19. package/src/export/shots.js +7 -2
  20. package/src/fiscal/normalize.js +19 -0
  21. package/src/fs-retry.js +37 -2
  22. package/src/import/office.js +5 -4
  23. package/src/layouts.js +104 -99
  24. package/src/lessons.js +1 -1
  25. package/src/library.js +7 -7
  26. package/src/map-analysis.js +4 -1
  27. package/src/map-config.js +10 -4
  28. package/src/map.js +21 -8
  29. package/src/mcp/server.js +17 -0
  30. package/src/motion.js +20 -6
  31. package/src/purpose.js +1 -1
  32. package/src/runtime/base.css +88 -332
  33. package/src/runtime/fit.js +3 -2
  34. package/src/runtime/fonts/amatic-sc.css +3 -0
  35. package/src/runtime/fonts/bebas-neue.css +2 -0
  36. package/src/runtime/fonts/cabin-sketch.css +2 -0
  37. package/src/runtime/fonts/fraunces.css +3 -0
  38. package/src/runtime/fonts/gelasio.css +3 -0
  39. package/src/runtime/fonts/ibm-plex-mono.css +4 -0
  40. package/src/runtime/fonts/index.json +10 -1
  41. package/src/runtime/fonts/itim.css +2 -0
  42. package/src/runtime/fonts/montserrat.css +2 -0
  43. package/src/runtime/fonts/space-grotesk.css +2 -0
  44. package/src/runtime/map-background.js +91 -0
  45. package/src/runtime/map.js +41 -9
  46. package/src/runtime/runtime.js +81 -100
  47. package/src/runtime/skins/caderno.css +33 -0
  48. package/src/runtime/skins/cartaz.css +27 -0
  49. package/src/runtime/skins/infografico.css +29 -0
  50. package/src/runtime/skins/lousa.css +22 -0
  51. package/src/runtime/skins/musgo.css +42 -0
  52. package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
  53. package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
  54. package/src/runtime/vendor/geo-contornos.json +1 -1
  55. package/src/runtime/vendor/maplibre-gl.css +1 -0
  56. package/src/runtime/vendor/maplibre-gl.js +825 -0
  57. package/src/runtime/vendor/maplibre-leaflet.js +234 -0
  58. package/src/runtime/vendor/maplibre-worker.js +10 -0
  59. package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
  60. package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
  61. package/src/studio/command-approval.js +2 -3
  62. package/src/studio/demo-decks.js +4 -2
  63. package/src/studio/export-jobs.js +86 -0
  64. package/src/studio/exporting.js +36 -18
  65. package/src/studio/feature-demos.js +144 -0
  66. package/src/studio/ia-routes.js +9 -0
  67. package/src/studio/layout-samples.js +9 -18
  68. package/src/studio/library-routes.js +0 -9
  69. package/src/studio/project.js +1 -0
  70. package/src/studio/public/ai-settings.js +2 -2
  71. package/src/studio/public/app.js +41 -51
  72. package/src/studio/public/export-status.js +154 -0
  73. package/src/studio/public/index.html +13 -10
  74. package/src/studio/public/library.css +12 -3
  75. package/src/studio/public/library.html +1 -0
  76. package/src/studio/public/library.js +36 -14
  77. package/src/studio/public/map-data.js +7 -3
  78. package/src/studio/public/map-editor.js +625 -236
  79. package/src/studio/public/map-settings.js +3 -1
  80. package/src/studio/public/slide-form.js +18 -15
  81. package/src/studio/public/style.css +54 -9
  82. package/src/studio/public/ui-icons.js +1 -1
  83. package/src/studio/server.js +27 -12
  84. package/src/studio/snapshot.js +8 -4
  85. package/src/studio/template-collections.js +1 -1
  86. package/src/studio/tour.js +89 -7
  87. package/src/themes.js +154 -0
  88. package/templates/cenario-e-ciencia.yaml +1 -21
  89. package/templates/ensaio-api.yaml +3 -3
  90. package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
  91. package/templates/cenario/imagens/cidade.svg +0 -30
  92. package/templates/cenario/imagens/pessoa.svg +0 -15
@@ -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", "scenography", "portal", "science", "kinetic", "video", "carousel", "duel", "terminals", "turns", "solution", "calc", "algo", "graphlab", "codelab", "playground",
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", scenography: "Texto no cenário", codewalk: "Código guiado", spotlight: "Foco guiado", kinetic: "Tipografia cinética",
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", "scenography", "portal", "kinetic"]],
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", kinetic: "beats" };
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 && !["kinetic", "statement"].includes(layoutName)) {
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
- await syncDeckToServer(); // a prévia é montada a partir do deck do servidor
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) return staleTab(data.error);
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
- // Painel lateral: Formatar, Assistente ou Variáveis
3639
- // abas do painel lateral: Formatar (conteúdo), Propriedades (inspetor), Assistente, Variáveis
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 && currentPane() === "chat");
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: levam alguns segundos (o Chrome invisível desenha cada slide)
4312
+ // PowerPoint / PDF / roteiro: acompanham a fila do servidor, sem prender a página no download.
4318
4313
  const EXPORTS = {
4319
- pptx: { label: "o PowerPoint", done: "PowerPoint editável, com animações e notas." },
4320
- pdf: { label: "o PDF", done: "PDF com um slide por página." },
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 x = clean ? { label: "o PowerPoint sem as notas", done: "PowerPoint sem as notas do apresentador, pronto para mandar." } : EXPORTS[kind];
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
- const { res, name } = await downloadFrom(`api/export/${kind}${clean ? "?notas=0" : ""}`, `apresentacao.${{ pptx: "pptx", tudo: "zip", "estudo-html": "html" }[kind] || "pdf"}`);
4336
- const warns = JSON.parse(decodeURIComponent(res.headers.get("X-Sagadeck-Warnings") || "%5B%5D"));
4337
- showToast(warns.length ? `"${name}" baixado, com ${warns.length} aviso(s): ${warns.slice(0, 2).join("; ")}` : `"${name}" baixado: ${x.done}`, warns.length ? 9000 : 4000);
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 ${x.label}: ${err.message}`, 8000);
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: biblioteca, Nova, Exemplo: aula de APIs"><i class="ic" data-ic="plug-zap"></i><span>Slide de API</span></button>
186
+ <button id="btn-api-slide" class="rbtn rbtn-lg" title="Slide que executa uma requisição de verdade (tipo Postman), com o código curl e Python ao lado. Exemplos prontos: Demos oficiais, APIs com curl"><i class="ic" data-ic="plug-zap"></i><span>Slide de API</span></button>
187
187
  <button id="btn-api-envs" class="rbtn rbtn-lg" title="Endereços, tokens e segredos dos slides de API (ficam na sua máquina, nunca no deck)"><i class="ic" data-ic="server-cog"></i><span>Ambientes</span></button>
188
188
  <button id="btn-api-vars-studio" class="rbtn rbtn-lg" title="Abrir o painel de variáveis do deck"><i class="ic" data-ic="braces"></i><span>Variáveis</span></button>
189
189
  </div>
@@ -456,18 +456,19 @@
456
456
 
457
457
  <aside id="inspector-sidebar" class="task-pane" aria-label="Painel lateral">
458
458
  <div class="pane-header">
459
- <div class="segmented" role="tablist">
460
- <!-- como no VS Code: a aba aberta mostra o nome; as outras, só o ícone (o nome fica na dica) -->
461
- <button id="tab-btn-props" class="seg active" role="tab" title="Formatar: o conteúdo do slide"><i class="ic" data-ic="pencil"></i><span class="seg-label">Formatar</span></button>
462
- <button id="tab-btn-inspect" class="seg" role="tab" title="Propriedades do objeto selecionado, do slide e da apresentação"><i class="ic" data-ic="sliders-horizontal"></i><span class="seg-label">Propriedades</span></button>
463
- <button id="tab-btn-chat" class="seg" role="tab" title="Assistente de IA"><i class="ic" data-ic="sparkles"></i><span class="seg-label">Assistente</span><span id="chat-badge" class="chat-badge-dot"></span></button>
464
- <button id="tab-btn-vars" class="seg" role="tab" title="Variáveis do ambiente"><i class="ic" data-ic="braces"></i><span class="seg-label">Variáveis</span></button>
459
+ <div class="pane-tabs" role="tablist" aria-label="Painéis de edição e Assistente">
460
+ <div class="segmented" id="property-tabs">
461
+ <button id="tab-btn-props" class="seg" role="tab" aria-selected="false" aria-label="Formatar" title="Formatar: o conteúdo do slide"><i class="ic" data-ic="pencil"></i><span class="seg-label">Formatar</span></button>
462
+ <button id="tab-btn-inspect" class="seg" role="tab" aria-selected="false" aria-label="Propriedades" title="Propriedades do objeto selecionado, do slide e da apresentação"><i class="ic" data-ic="sliders-horizontal"></i><span class="seg-label">Propriedades</span></button>
463
+ <button id="tab-btn-vars" class="seg" role="tab" aria-selected="false" aria-label="Variáveis" title="Variáveis do ambiente"><i class="ic" data-ic="braces"></i><span class="seg-label">Variáveis</span></button>
464
+ </div>
465
+ <button id="tab-btn-chat" class="seg active" role="tab" aria-selected="true" title="Assistente de IA"><i class="ic" data-ic="sparkles"></i><span class="seg-label">Assistente</span><span id="chat-badge" class="chat-badge-dot"></span></button>
465
466
  </div>
466
467
  <button id="btn-close-pane" class="icon-btn" title="Fechar painel"><i class="ic" data-ic="x"></i></button>
467
468
  </div>
468
469
 
469
470
  <div id="tab-panel-inspect" class="tab-panel inspector-panel" role="tabpanel" aria-labelledby="tab-btn-inspect"><div id="inspector-body" class="ip-body"></div></div>
470
- <div id="tab-panel-props" class="tab-panel active">
471
+ <div id="tab-panel-props" class="tab-panel" role="tabpanel" aria-labelledby="tab-btn-props">
471
472
  <div id="auto-banner" class="auto-banner" hidden></div>
472
473
  <div id="slide-fields-form" class="fields-form"></div>
473
474
  <div class="pane-section">
@@ -476,7 +477,7 @@
476
477
  </div>
477
478
  </div>
478
479
 
479
- <div id="tab-panel-chat" class="tab-panel">
480
+ <div id="tab-panel-chat" class="tab-panel active" role="tabpanel" aria-labelledby="tab-btn-chat">
480
481
  <div class="chat-options">
481
482
  <button id="btn-brainstorm-apply" class="btn btn-primary btn-sm hidden" type="button" title="Autoriza a IA a aplicar o que vocês combinaram na conversa">Pode fazer</button>
482
483
  </div>
@@ -813,7 +814,7 @@
813
814
  </div>
814
815
  <div class="modal-dialog-body">
815
816
  <label class="field-label" for="ai-deck-briefing">Sobre o que é, para quem e com que objetivo?</label>
816
- <textarea id="ai-deck-briefing" class="form-control" rows="5" placeholder="ex.: Palestra de 20 minutos para gerentes de agência sobre usar IA com segurança no atendimento. Comece com um caso real, mostre 3 cuidados e feche com um checklist.&#10;(imagens: peça no texto — 'com fotos', 'você decide onde ilustrar'… sem pedir, não gera)"></textarea>
817
+ <textarea id="ai-deck-briefing" class="form-control" rows="5" placeholder="ex.: Palestra de 20 minutos para líderes de equipe sobre usar IA com segurança no dia a dia. Comece com um caso real, mostre 3 cuidados e feche com um checklist.&#10;(imagens: peça no texto — 'com fotos', 'você decide onde ilustrar'… sem pedir, não gera)"></textarea>
817
818
  <details id="ai-deck-advanced">
818
819
  <summary>Opções avançadas (minutos, estilo, arquivos…)</summary>
819
820
  <div class="form-row">
@@ -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
- body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-template-rows: 84px minmax(0, 1fr); height: 100vh; height: 100dvh; overflow: hidden; background: var(--canvas-bg); font-size: 14px; }
15
+ /* a faixa "Configure a IA" tem a linha dela (largura toda; some quando escondida); lateral e conteúdo vêm depois */
16
+ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-template-rows: 84px auto minmax(0, 1fr); height: 100vh; height: 100dvh; overflow: hidden; background: var(--canvas-bg); font-size: 14px; }
16
17
  .skip-link { position: fixed; top: -80px; left: 20px; z-index: 100; padding: 12px; background: var(--chrome); border-radius: 8px; }
17
18
  .skip-link:focus { top: 15px; }
18
19
  .lib-brand { display: flex; align-items: center; gap: 11px; padding: 0 24px; border-right: 1px solid var(--ui-line); text-decoration: none; font-size: 23px; font-weight: 720; letter-spacing: -.8px; }
@@ -28,7 +29,7 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
28
29
  .lib-search { display: flex; align-items: center; gap: 9px; width: 228px; height: 37px; color: var(--text-3); padding: 0 11px; background: transparent; border: 1px solid var(--ui-line); border-radius: 8px; }
29
30
  .lib-search input { background: none; color: var(--text); border: 0; outline: 0; width: 100%; min-width: 0; font-size: 11px; }
30
31
  .lib-search kbd { border: 1px solid var(--ui-line); border-radius: 3px; font-size: 10px; padding: 0 4px; }
31
- .ai-banner { display: flex; align-items: center; gap: 12px; margin: 10px 24px 0; padding: 10px 14px; border-radius: 10px; background: #fef3c7; color: #78350f; border: 1px solid #f59e0b; font-size: 13px; }
32
+ .ai-banner { grid-column: 1 / -1; grid-row: 2; display: flex; align-items: center; gap: 12px; margin: 10px 24px 0; padding: 10px 14px; border-radius: 10px; background: #fef3c7; color: #78350f; border: 1px solid #f59e0b; font-size: 13px; }
32
33
  .ai-banner[hidden] { display: none; }
33
34
  .ai-banner span { flex: 1; min-width: 0; overflow-wrap: anywhere; } /* o motivo pode trazer um caminho longo */
34
35
  .ai-banner button { flex: none; }
@@ -45,6 +46,7 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
45
46
  .lib-user { display: flex; align-items: center; gap: 8px; font-size: 12px; }
46
47
  .lib-avatar { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--selected); color: var(--brand); text-transform: uppercase; font-weight: 700; }
47
48
  .lib-body { display: contents; }
49
+ .lib-side, .lib-main { grid-row: 3; }
48
50
  .lib-side { min-height: 0; display: flex; flex-direction: column; padding: 14px 14px 18px; border-right: 1px solid var(--ui-line); overflow-y: auto; }
49
51
  #side { display: flex; flex-direction: column; gap: 5px; flex: 1; min-height: 360px; }
50
52
  .nav { display: flex; align-items: center; gap: 10px; min-height: 40px; flex: none; padding: 0 13px; border-radius: 8px; color: var(--text-2); width: 100%; text-align: left; background: none; border: 0; font-size: 12px; }
@@ -231,7 +233,7 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
231
233
  @media (min-width: 1680px) { .lib-main { padding: 40px 65px 70px; } .hero { padding-bottom: 48px; } .hero-stage { min-height: 320px; } .mini-dark { width: 335px; height: 207px; } .mini-dark b { font-size: 45px; } .mini-beige { left: 8%; } .mini-pop { right: 10%; } }
232
234
  @media (max-width: 1200px) { body.lib { grid-template-columns: 190px minmax(0, 1fr); } .lib-top { padding: 0 24px; gap: 9px; } .lib-brand { padding: 0 20px; } .lib-main { padding: 30px 28px 60px; } .breadcrumb { display: none; } .hero { gap: 20px; } .hero-stage { transform: scale(.87); transform-origin: center right; margin-left: -40px; } .hero h1 { font-size: 44px; } .section-head h2 { font-size: 18px; } .experience-art { padding: 18px; } .experiences-grid { gap: 17px; } }
233
235
  @media (max-width: 940px) { .hero { grid-template-columns: 1fr; padding-bottom: 30px; } .hero h1 { font-size: 45px; max-width: 480px; } .hero-stage { display: none; } .hero p { max-width: 420px; } .experiences-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .experience-art { padding: 20px; } .empty.compact { flex-wrap: wrap; } }
234
- @media (max-width: 700px) { body.lib { grid-template-columns: 1fr; grid-template-rows: 70px minmax(0, 1fr); } .lib-brand { position: absolute; left: 8px; height: 70px; padding: 0 10px; border: 0; font-size: 20px; } .lib-brand small { font-size: 6px; } .lib-logo { width: 27px; } .lib-top { padding: 0 15px 0 168px; gap: 7px; } .lib-top .spacer { display: none; } .lib-search { width: 100%; min-width: 0; padding: 0 8px; border: 0; } .lib-search kbd, #btn-new .lbl, .lib-user .uname, #btn-theme { display: none; } #btn-new { padding: 0; min-width: 35px; min-height: 35px; } .lib-body { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } .lib-side { padding: 8px 13px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--ui-line); flex-direction: row; } #side { flex-direction: row; align-items: center; min-height: 0; flex: none; gap: 4px; } .nav { min-height: 33px; width: auto; white-space: nowrap; font-size: 11px; gap: 7px; padding: 0 11px; } .nav .ic { width: 15px; height: 15px; } .nav .count { display: none; } .side-title { margin: 0 3px; font-size: 0; } .side-title button { width: 25px; height: 30px; } .lib-side .grow, .studio-note, .where, .ai-settings { display: none; } .side-foot { padding: 0; border: 0; } .lib-main { padding: 27px 22px 45px; } .hero { padding-top: 0; } .hero h1 { font-size: 41px; letter-spacing: -1.8px; } .hero p { font-size: 12px; margin: 16px 0 21px; } .eyebrow { font-size: 8px; margin-bottom: 17px; } .hero-actions { gap: 15px; } .section-head { gap: 10px; align-items: flex-start; } .section-head h2 { font-size: 17px; max-width: 230px; } .section-head .text-button { font-size: 10px; } .experiences-section { padding-top: 24px; } .experience-filters { gap: 2px; margin-bottom: 17px; } .experience-filter { padding: 7px 9px; font-size: 9px; } .experiences-grid { gap: 17px 12px; } .experience-art { padding: 13px; } .experience-art b { margin-top: 13px; } .experience-art small { letter-spacing: .8px; } .experience-meta { padding-top: 9px; } .experience-meta strong { font-size: 10px; } .experience-meta .experience-sub { font-size: 8px; line-height: 1.4; } .experience-meta .arrow { display: none; } .pop-star { font-size: 53px; } .pop-mark { border-width: 18px; } .code-cursor { width: 4px; height: 9px; left: 14px; bottom: 16px; } .code-lines { display: none; } .executive-bars { right: 11px; bottom: 13px; } .experience-art b { font-size: 10cqw; } .catalog-heading h1 { font-size: 30px; } .catalog-heading p { font-size: 12px; } .head h1 { font-size: 23px; } .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 12px; } .empty.compact { padding: 18px; gap: 12px; } .empty.compact .actions { justify-content: flex-start; } .empty.compact .empty-copy { min-width: 170px; } .over { padding: 6px; gap: 4px; opacity: 1; } .over .pill { font-size: 8px; padding: 0 6px; min-height: 25px; } .pill .ic { width: 10px; height: 10px; } .meta .info { font-size: 9px; } .meta .name { font-size: 11px; } .chip { font-size: 9px; } .ldlg { padding: 22px; } .ldlg .row .lib-btn { font-size: 11px; } }
236
+ @media (max-width: 700px) { body.lib { grid-template-columns: 1fr; grid-template-rows: 70px auto auto minmax(0, 1fr); } .lib-main { grid-row: 4; } .lib-brand { position: absolute; left: 8px; height: 70px; padding: 0 10px; border: 0; font-size: 20px; } .lib-brand small { font-size: 6px; } .lib-logo { width: 27px; } .lib-top { padding: 0 15px 0 168px; gap: 7px; } .lib-top .spacer { display: none; } .lib-search { width: 100%; min-width: 0; padding: 0 8px; border: 0; } .lib-search kbd, #btn-new .lbl, .lib-user .uname, #btn-theme { display: none; } #btn-new { padding: 0; min-width: 35px; min-height: 35px; } .lib-body { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } .lib-side { padding: 8px 13px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--ui-line); flex-direction: row; } #side { flex-direction: row; align-items: center; min-height: 0; flex: none; gap: 4px; } .nav { min-height: 33px; width: auto; white-space: nowrap; font-size: 11px; gap: 7px; padding: 0 11px; } .nav .ic { width: 15px; height: 15px; } .nav .count { display: none; } .side-title { margin: 0 3px; font-size: 0; } .side-title button { width: 25px; height: 30px; } .lib-side .grow, .studio-note, .where, .ai-settings { display: none; } .side-foot { padding: 0; border: 0; } .lib-main { padding: 27px 22px 45px; } .hero { padding-top: 0; } .hero h1 { font-size: 41px; letter-spacing: -1.8px; } .hero p { font-size: 12px; margin: 16px 0 21px; } .eyebrow { font-size: 8px; margin-bottom: 17px; } .hero-actions { gap: 15px; } .section-head { gap: 10px; align-items: flex-start; } .section-head h2 { font-size: 17px; max-width: 230px; } .section-head .text-button { font-size: 10px; } .experiences-section { padding-top: 24px; } .experience-filters { gap: 2px; margin-bottom: 17px; } .experience-filter { padding: 7px 9px; font-size: 9px; } .experiences-grid { gap: 17px 12px; } .experience-art { padding: 13px; } .experience-art b { margin-top: 13px; } .experience-art small { letter-spacing: .8px; } .experience-meta { padding-top: 9px; } .experience-meta strong { font-size: 10px; } .experience-meta .experience-sub { font-size: 8px; line-height: 1.4; } .experience-meta .arrow { display: none; } .pop-star { font-size: 53px; } .pop-mark { border-width: 18px; } .code-cursor { width: 4px; height: 9px; left: 14px; bottom: 16px; } .code-lines { display: none; } .executive-bars { right: 11px; bottom: 13px; } .experience-art b { font-size: 10cqw; } .catalog-heading h1 { font-size: 30px; } .catalog-heading p { font-size: 12px; } .head h1 { font-size: 23px; } .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 12px; } .empty.compact { padding: 18px; gap: 12px; } .empty.compact .actions { justify-content: flex-start; } .empty.compact .empty-copy { min-width: 170px; } .over { padding: 6px; gap: 4px; opacity: 1; } .over .pill { font-size: 8px; padding: 0 6px; min-height: 25px; } .pill .ic { width: 10px; height: 10px; } .meta .info { font-size: 9px; } .meta .name { font-size: 11px; } .chip { font-size: 9px; } .ldlg { padding: 22px; } .ldlg .row .lib-btn { font-size: 11px; } }
235
237
  @media (max-width: 390px) { .lib-search input { font-size: 10px; } .lib-main { padding-left: 17px; padding-right: 17px; } .hero h1 { font-size: 36px; } .lib-brand { font-size: 18px; gap: 6px; } .lib-top { padding-left: 149px; } .hero-actions { gap: 12px; } .hero-actions .lib-btn { font-size: 11px; } .section-head p { line-height: 1.6; } }
236
238
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; } }
237
239
  .lmenu.tall { max-width: 340px; } /* menu Nova: descrições quebram em vez de alargar o menu */
@@ -278,3 +280,10 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
278
280
  .dlg-ask-opts { display: flex; flex-wrap: wrap; gap: 6px; }
279
281
  .dlg-ask-free { display: flex; gap: 6px; }
280
282
  .dlg-ask-free input { flex: 1; }
283
+ /* demos oficiais (só leitura): selo com cadeado, descrição numa linha, a pasta da lateral */
284
+ .card-oficial .thumb img { width: 100%; height: 100%; object-fit: cover; }
285
+ .chip-oficial { font-weight: 600; color: var(--accent, #5b4bdb); gap: 3px; }
286
+ .chip-oficial .ic, .head h1 .ic { width: 1em; height: 1em; }
287
+ .card-oficial .meta .info { min-width: 0; }
288
+ .card-oficial .meta .info .desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
289
+ .grid-oficial { margin-top: 18px; }
@@ -55,6 +55,7 @@
55
55
  <script src="update-notice.js"></script>
56
56
  <script src="ai-settings.js"></script>
57
57
  <script src="map-settings.js"></script>
58
+ <script src="export-status.js"></script>
58
59
  <script src="library.js"></script>
59
60
  </body>
60
61
  </html>