sagadeck 1.7.0 → 1.8.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 +41 -1
- package/SKILL.md +3 -3
- package/bin/sagadeck.js +2 -2
- package/docs/ANDAMENTO.md +162 -4
- package/docs/EXPERIENCIAS.md +1 -1
- package/docs/REFERENCIA.md +59 -1
- package/package.json +2 -1
- package/src/ai/deck-ai.js +13 -4
- package/src/ai/ia-config.js +17 -2
- package/src/ai/llm.js +6 -4
- package/src/ai/map-analysis-run.js +119 -0
- package/src/ai/map-data.js +221 -0
- package/src/ai/variety.js +1 -1
- package/src/build.js +6 -2
- package/src/experiences.js +1 -1
- package/src/export/browser.js +23 -1
- package/src/export/pptx.js +2 -1
- package/src/export/shots.js +3 -1
- package/src/layouts.js +10 -0
- package/src/library.js +2 -2
- package/src/map-analysis.js +106 -0
- package/src/map-config.js +83 -0
- package/src/map-routing.js +82 -0
- package/src/map-services.js +60 -0
- package/src/map.js +261 -0
- package/src/mcp/server.js +4 -4
- package/src/runtime/base.css +21 -1
- package/src/runtime/code-lab.js +10 -10
- package/src/runtime/map.js +234 -0
- package/src/runtime/runtime.js +1 -1
- package/src/runtime/skins/aluminio.css +1 -1
- package/src/runtime/skins/prata.css +1 -1
- package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
- package/src/runtime/vendor/geo-contornos.json +1 -0
- package/src/runtime/vendor/leaflet.css +661 -0
- package/src/runtime/vendor/leaflet.js +6 -0
- package/src/studio/ai-chat-route.js +20 -0
- package/src/studio/banner.js +1 -1
- package/src/studio/demo-decks.js +3 -1
- package/src/studio/geo-routes.js +138 -0
- package/src/studio/ia-routes.js +4 -1
- package/src/studio/layout-samples.js +71 -0
- package/src/studio/project.js +1 -1
- package/src/studio/public/ai-settings.js +15 -2
- package/src/studio/public/app.js +13 -7
- package/src/studio/public/chat.js +1 -1
- package/src/studio/public/explorer.js +23 -8
- package/src/studio/public/index.html +4 -0
- package/src/studio/public/library.css +2 -1
- package/src/studio/public/library.html +2 -0
- package/src/studio/public/library.js +10 -1
- package/src/studio/public/map-data.js +154 -0
- package/src/studio/public/map-editor.js +287 -0
- package/src/studio/public/map-fields.js +21 -0
- package/src/studio/public/map-settings.js +84 -0
- package/src/studio/public/slide-form.js +1 -1
- package/src/studio/public/style.css +13 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/public/viewers.js +2 -1
- package/src/studio/public/visual-editor.js +1 -1
- package/src/studio/server.js +13 -1
- package/src/studio/tour.js +76 -0
- package/src/templates/scaffold.js +5 -5
- package/src/themes.js +5 -5
- package/templates/{exemplo-keynote.yaml → exemplo-prata.yaml} +6 -6
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
// ------------------------------------------------------------------ planilha como no Excel
|
|
244
244
|
const colName = (i) => { let s = ""; i++; while (i > 0) { const r = (i - 1) % 26; s = String.fromCharCode(65 + r) + s; i = Math.floor((i - 1) / 26); } return s; };
|
|
245
245
|
const looksNum = (v) => /^\s*[-+]?(R\$\s*)?(\d{1,3}(\.\d{3})+(,\d+)?|\d+([.,]\d+)?)\s*%?\s*$/.test(String(v ?? ""));
|
|
246
|
-
function sheetGrid(host, { rows, editable = false, hasHeader = false, maxRender = 3000, onChange, types = [] } = {}) {
|
|
246
|
+
function sheetGrid(host, { rows, editable = false, hasHeader = false, maxRender = 3000, onChange, onSelect, types = [] } = {}) {
|
|
247
247
|
let data = rows.map((r) => [...r]);
|
|
248
248
|
const undo = [], redo = [];
|
|
249
249
|
let sel = { r: 0, c: 0, r2: 0, c2: 0 }, editing = null;
|
|
@@ -267,6 +267,7 @@
|
|
|
267
267
|
const [r1, r2] = [Math.min(sel.r, sel.r2), Math.max(sel.r, sel.r2)], [c1, c2] = [Math.min(sel.c, sel.c2), Math.max(sel.c, sel.c2)];
|
|
268
268
|
for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) cell(r, c)?.classList.add("sel");
|
|
269
269
|
const cur = cell(sel.r, sel.c); cur?.classList.add("cur");
|
|
270
|
+
onSelect?.(sel.r, sel.c); // quem abriu a planilha acompanha a linha (ex.: o mapa abre o cartão do ponto)
|
|
270
271
|
for (let c = c1; c <= c2; c++) table.querySelector(`thead th[data-col="${c}"]`)?.classList.add("on");
|
|
271
272
|
for (let r = r1; r <= r2; r++) table.querySelector(`tbody th[data-row="${r}"]`)?.classList.add("on");
|
|
272
273
|
cur?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
@@ -257,7 +257,7 @@
|
|
|
257
257
|
if(!root.dataset.visualEvents){
|
|
258
258
|
root.dataset.visualEvents='true';
|
|
259
259
|
root.addEventListener('pointerdown',e=>{
|
|
260
|
-
if(e.button!==0 || e.target.closest('.science-plot,input,textarea'))return;
|
|
260
|
+
if(e.button!==0 || e.target.closest('.science-plot,.map-live,input,textarea'))return; // gráfico e mapa ao vivo recebem o próprio clique
|
|
261
261
|
if(e.target.closest('[contenteditable="true"]')?.dataset.writing)return; // escrevendo: o clique é do cursor
|
|
262
262
|
const el=objectAt(e.clientX,e.clientY)||(locked(e.target.closest('[data-vkey]'))?null:e.target.closest('[data-vkey]'));
|
|
263
263
|
if(!el){
|
package/src/studio/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import {chatErrorResult,installErrorResponses} from './errors.js';
|
|
|
6
6
|
import { slideCopyRoutes } from "./slide-copy-routes.js";
|
|
7
7
|
import { publicScript } from "./public-files.js";
|
|
8
8
|
import { iaRoutes } from "./ia-routes.js";
|
|
9
|
+
import { geoRoutes } from "./geo-routes.js";
|
|
9
10
|
import { codeWatch } from "./code-version.js";
|
|
10
11
|
import {meetingRoutes,meetingPreview} from '../meeting.js';
|
|
11
12
|
import { codeRoutes } from "./code-routes.js";
|
|
@@ -493,6 +494,13 @@ export function createStudioServer(deckPath = null, opts = {}) {
|
|
|
493
494
|
res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/diagram.js" ? "diagram.js" : "vendor/mermaid.min.js")));
|
|
494
495
|
return;
|
|
495
496
|
}
|
|
497
|
+
// mapa ao vivo (src/runtime/map.js): o Leaflet e os contornos só são pedidos quando um slide de mapa monta
|
|
498
|
+
if (["/map.js", "/leaflet.js", "/leaflet.css", "/geo-contornos.json"].includes(pathname)) {
|
|
499
|
+
const type = pathname.endsWith(".css") ? "text/css" : pathname.endsWith(".json") ? "application/json" : "application/javascript";
|
|
500
|
+
res.writeHead(200, { "Content-Type": `${type}; charset=utf-8` }); // sem cache: depois de atualizar, o mapa novo vale na hora
|
|
501
|
+
res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/map.js" ? "map.js" : `vendor${pathname}`)));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
496
504
|
if (["/science.js", "/formula.js", "/calc.js", "/plotly.min.js"].includes(pathname)) {
|
|
497
505
|
res.writeHead(200, {"Content-Type":"application/javascript; charset=utf-8"});
|
|
498
506
|
res.end(fs.readFileSync(path.join(RUNTIME_DIR, pathname === "/plotly.min.js" ? "vendor/plotly.min.js" : pathname.slice(1))));
|
|
@@ -895,12 +903,16 @@ export function createStudioServer(deckPath = null, opts = {}) {
|
|
|
895
903
|
|
|
896
904
|
// Configurar IA (provedor, chave, modelos desta máquina): src/studio/ia-routes.js
|
|
897
905
|
if (pathname.startsWith("/api/ia") && await iaRoutes({ req, res, pathname, opts, readJSON })) return;
|
|
906
|
+
// Configurar mapa, estado dos serviços e coordenadas pelo endereço: src/studio/geo-routes.js
|
|
907
|
+
if (pathname.startsWith("/api/mapa") && await geoRoutes({ req, res, pathname, opts, readJSON, W, isBundledTemplate })) return;
|
|
898
908
|
|
|
899
909
|
if (pathname === "/api/ai/status" && req.method === "GET") {
|
|
900
910
|
const cfg = llmConfig();
|
|
901
911
|
const available = await llmAvailable({ force: url.searchParams.has("refresh") });
|
|
902
912
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
903
|
-
|
|
913
|
+
// no servidor, só se está ligada: provedor e modelos são de quem administra (src/studio/ia-routes.js)
|
|
914
|
+
if (opts.multiuser) { res.end(JSON.stringify({ available, configured: llmConfigured(cfg), server: true })); return; }
|
|
915
|
+
res.end(JSON.stringify({ available, configured: llmConfigured(cfg), problem: cfg.problem || "", source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
|
|
904
916
|
return;
|
|
905
917
|
}
|
|
906
918
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// "Conheça o SagaDeck": a apresentação que todo usuário acha na biblioteca, com tudo o que dá para fazer. Monta-se
|
|
2
|
+
// sozinha dos exemplos de layout (layout-samples.js): layout novo entra aqui sem ninguém lembrar (engine.test.js
|
|
3
|
+
// confere). Cada slide diz o nome do recurso e a frase para pedir à IA. Abre em prévia, como os modelos da vitrine.
|
|
4
|
+
import { LAYOUT_INFO, LAYOUT_SAMPLES, LAYOUT_ASK } from "./layout-samples.js";
|
|
5
|
+
|
|
6
|
+
export const TOUR_TITLE = "Conheça o SagaDeck";
|
|
7
|
+
|
|
8
|
+
// os grupos na ordem em que fazem sentido para quem está conhecendo; layout que não estiver em nenhum entra em "Mais"
|
|
9
|
+
const GROUPS = [
|
|
10
|
+
["Abrir e fechar", "capa, seções, agenda e encerramento", ["cover", "section", "agenda", "end", "references"]],
|
|
11
|
+
["Uma ideia por vez", "frases, números e palavras que ocupam o palco", ["statement", "headline", "quote", "definition", "number", "kinetic", "scenography"]],
|
|
12
|
+
["Organizar o conteúdo", "listas, cartões, etapas, comparações e linhas do tempo", ["list", "cards", "mosaic", "ribbon", "bento", "split", "compare", "matrix", "steps", "timeline", "calendar", "hub", "infographic", "pyramid", "funnel"]],
|
|
13
|
+
["Números e dados", "indicadores, gráficos, tabelas e painéis", ["stats", "chart", "table", "map", "onepage", "status", "poster"]],
|
|
14
|
+
["Imagem e vídeo", "fotos, vídeos e foco guiado", ["image", "full", "spotlight", "carousel", "video"]],
|
|
15
|
+
["Com a plateia", "perguntas, enquetes e experiências interativas", ["question", "poll", "decisionlab", "playground", "portal"]],
|
|
16
|
+
["Código e tecnologia", "código, APIs ao vivo, Git e diagramas", ["code", "codewalk", "codelab", "api", "duel", "terminals", "turns", "diagram", "graphlab", "algo", "dossier"]],
|
|
17
|
+
["Aula e ciência", "fórmulas que viram curva, calculadoras e exercícios", ["science", "calc", "solution"]],
|
|
18
|
+
["Do seu jeito", "monte o slide livremente", ["blocks", "canvas"]],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// o que não é layout, mas a pessoa precisa saber que existe
|
|
22
|
+
const EXTRAS = [
|
|
23
|
+
["sparkles", "Criar com IA", "De um tema, de um arquivo (PDF, Word, planilha, PowerPoint) ou de um link: a IA monta a apresentação inteira."],
|
|
24
|
+
["message-circle", "Conversar com a IA", "No chat, peça mudanças no slide ou no deck todo; cada mudança vira uma versão que dá para desfazer."],
|
|
25
|
+
["search-check", "Leitura crítica", "A IA revisa a apresentação e aponta o que melhorar, slide a slide, com Aplicar e Ignorar."],
|
|
26
|
+
["file-up", "Importar PowerPoint", "Traga um .pptx e continue editando aqui, com as imagens e a ordem do original."],
|
|
27
|
+
["table", "Gráfico da planilha", "Cole do Excel ou use a planilha do projeto: o Studio sugere gráficos e atualiza quando o arquivo muda."],
|
|
28
|
+
["palette", "Temas e paletas", "Troque o visual inteiro com um clique; o conteúdo se rearruma sozinho."],
|
|
29
|
+
["presentation", "Modo apresentador", "Notas, próximo slide, cronômetro e caneta para desenhar ao vivo."],
|
|
30
|
+
["download", "PDF e PowerPoint", "Exporte para PDF ou para um PowerPoint editável, com as animações de clique."],
|
|
31
|
+
["link", "Compartilhar link", "Um link só de leitura, para quem não precisa editar."],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// os exemplos de navegação (hub, cartões) apontam para slides que não existem aqui: o link sai, o desenho fica
|
|
35
|
+
const semLinks = (v) => (Array.isArray(v) ? v.map(semLinks) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([k]) => k !== "goto").map(([k, x]) => [k, semLinks(x)])) : v);
|
|
36
|
+
|
|
37
|
+
const asked = (ask) => `Peça à IA: “${ask}”`;
|
|
38
|
+
|
|
39
|
+
export function tourDeck() {
|
|
40
|
+
const all = Object.keys(LAYOUT_SAMPLES);
|
|
41
|
+
const used = new Set(GROUPS.flatMap(([, , names]) => names));
|
|
42
|
+
const groups = [...GROUPS.map(([title, sub, names]) => [title, sub, names.filter((n) => LAYOUT_SAMPLES[n])])];
|
|
43
|
+
const rest = all.filter((n) => !used.has(n));
|
|
44
|
+
if (rest.length) groups.push(["Mais recursos", "os layouts mais novos", rest]);
|
|
45
|
+
|
|
46
|
+
const slides = [{
|
|
47
|
+
layout: "cover", kicker: "SAGADECK · TOUR", title: TOUR_TITLE,
|
|
48
|
+
subtitle: "Tudo o que dá para fazer, um slide por recurso. Cada um diz como pedir à IA.",
|
|
49
|
+
figure: { icon: "compass", size: 320 },
|
|
50
|
+
notes: "Navegue à vontade: nada aqui cria arquivo. Embaixo de cada slide (nestas notas) está o nome do recurso e a frase para pedir à IA na sua apresentação.",
|
|
51
|
+
}];
|
|
52
|
+
groups.forEach(([title, sub, names], i) => {
|
|
53
|
+
if (!names.length) return;
|
|
54
|
+
slides.push({ layout: "section", number: i + 1, kicker: `${names.length} recurso${names.length === 1 ? "" : "s"}`, title, subtitle: sub });
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const sample = semLinks(structuredClone(LAYOUT_SAMPLES[name]));
|
|
57
|
+
const [label, desc] = LAYOUT_INFO[name] || [name, ""];
|
|
58
|
+
const ask = LAYOUT_ASK[name] || "";
|
|
59
|
+
slides.push({
|
|
60
|
+
...sample,
|
|
61
|
+
layout: name,
|
|
62
|
+
tour: name,
|
|
63
|
+
source: `**${label}**: ${desc}. ${asked(ask)}`,
|
|
64
|
+
// as notas ficam visíveis logo abaixo do slide no Studio (e na visão do apresentador): valem para todo layout,
|
|
65
|
+
// inclusive os que não desenham o rodapé (capa, citação, vídeo…)
|
|
66
|
+
notes: [`${label}: ${desc}.`, asked(ask), sample.notes].filter(Boolean).join("\n\n"),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
slides.push({ layout: "section", number: groups.length + 1, kicker: "Além dos slides", title: "O que mais o SagaDeck faz", subtitle: "IA, importação, exportação e apresentação" });
|
|
71
|
+
slides.push({ layout: "cards", title: "Com a IA", cols: 3, items: EXTRAS.slice(0, 3).map(([icon, title, text]) => ({ icon, title, text })) });
|
|
72
|
+
slides.push({ layout: "cards", title: "Com os seus arquivos", cols: 3, items: EXTRAS.slice(3, 6).map(([icon, title, text]) => ({ icon, title, text })) });
|
|
73
|
+
slides.push({ layout: "cards", title: "Na hora de apresentar", cols: 3, items: EXTRAS.slice(6).map(([icon, title, text]) => ({ icon, title, text })) });
|
|
74
|
+
slides.push({ layout: "end", title: "Agora é a sua vez.", subtitle: "Crie uma apresentação e peça à IA o que você viu aqui." });
|
|
75
|
+
return { title: TOUR_TITLE, theme: "sinal", duration: 30, maxWords: 140, slides };
|
|
76
|
+
}
|
|
@@ -6,7 +6,7 @@ export function generateScaffold({
|
|
|
6
6
|
title = "Minha Apresentação",
|
|
7
7
|
theme = "prata",
|
|
8
8
|
author = "Seu Nome",
|
|
9
|
-
type = "
|
|
9
|
+
type = "lancamento",
|
|
10
10
|
} = {}) {
|
|
11
11
|
const normType = String(type).toLowerCase();
|
|
12
12
|
|
|
@@ -243,7 +243,7 @@ export function generateScaffold({
|
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
// Padrão:
|
|
246
|
+
// Padrão: lançamento, limpo e com muito respiro (tema prata)
|
|
247
247
|
return {
|
|
248
248
|
title,
|
|
249
249
|
author,
|
|
@@ -252,7 +252,7 @@ export function generateScaffold({
|
|
|
252
252
|
slides: [
|
|
253
253
|
{
|
|
254
254
|
layout: "cover",
|
|
255
|
-
kicker: "
|
|
255
|
+
kicker: "Lançamento",
|
|
256
256
|
title: "Design com ==Propósito==.",
|
|
257
257
|
subtitle: "A harmonia entre simplicidade, respiro e precisão",
|
|
258
258
|
author,
|
|
@@ -355,14 +355,14 @@ export function generateScaffold({
|
|
|
355
355
|
text: "O padrão definitivo para criadores exigentes.",
|
|
356
356
|
rating: 5,
|
|
357
357
|
ratingLabel: "5.0 Pro",
|
|
358
|
-
tags: ["
|
|
358
|
+
tags: ["Lançamento", "Prata"],
|
|
359
359
|
},
|
|
360
360
|
],
|
|
361
361
|
notes: "> O tom escuro cria um momento teatral no palco.",
|
|
362
362
|
},
|
|
363
363
|
{
|
|
364
364
|
layout: "end",
|
|
365
|
-
title: "
|
|
365
|
+
title: "Simples assim.",
|
|
366
366
|
subtitle: "Obrigado.",
|
|
367
367
|
contacts: ["apresentacao@empresa.com", "suporte@empresa.com"],
|
|
368
368
|
notes: "> Conclusão elegante e contato.",
|
package/src/themes.js
CHANGED
|
@@ -270,7 +270,7 @@ export const THEMES = {
|
|
|
270
270
|
},
|
|
271
271
|
|
|
272
272
|
prata: {
|
|
273
|
-
label: "Prata —
|
|
273
|
+
label: "Prata — limpo e espacial: prata acetinado, muito respiro, tipografia do sistema / Inter limpa e elegante",
|
|
274
274
|
colors: {
|
|
275
275
|
paper: "F5F5F7", ink: "1D1D1F", accent: "0071E3", alert: "FF3B30",
|
|
276
276
|
muted: "86868B", line: "D2D2D7", surface: "FFFFFF", surfaceDark: "161617",
|
|
@@ -293,10 +293,10 @@ export const THEMES = {
|
|
|
293
293
|
},
|
|
294
294
|
radius: 20, deco: "silver",
|
|
295
295
|
},
|
|
296
|
-
// Alumínio: a cara
|
|
297
|
-
// cromados, grafite no escuro). O prata segue
|
|
296
|
+
// Alumínio: a cara de lançamento de aparelho premium (prata metálico, alumínio escovado, títulos
|
|
297
|
+
// cromados, grafite no escuro). O prata segue o branco limpo.
|
|
298
298
|
aluminio: {
|
|
299
|
-
label: "Alumínio — lançamento de
|
|
299
|
+
label: "Alumínio — lançamento de aparelho premium: prata metálico, títulos cromados, alumínio escovado e grafite",
|
|
300
300
|
colors: {
|
|
301
301
|
paper: "E4E5E9", ink: "1C1C1E", accent: "0066CC", alert: "D70015",
|
|
302
302
|
muted: "5E5E63", line: "C3C5CA", surface: "F4F5F7", surfaceDark: "2C2C2E",
|
|
@@ -305,7 +305,7 @@ export const THEMES = {
|
|
|
305
305
|
tones: {
|
|
306
306
|
light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "D1D3D8", em: "accent", onHi: "ink" },
|
|
307
307
|
dark: { bg: "1C1C1E", fg: "F5F5F7", muted: "A1A1A6", line: "3A3A3C", surface: "surfaceDark", hi: "48484A", em: "2997FF", onHi: "FFFFFF" },
|
|
308
|
-
// azul-pacífico
|
|
308
|
+
// azul-pacífico metálico (a pele põe o brilho)
|
|
309
309
|
accent: { bg: "1D3A50", fg: "FFFFFF", muted: "C5D6E3", line: "3F6580", surface: "2A4C64", hi: "FFFFFF", em: "8FC1E3", onHi: "1D3A50" },
|
|
310
310
|
alert: { bg: "alert", fg: "FFFFFF", muted: "FFD1D4", line: "F0525F", surface: "B30012", hi: "FFFFFF", em: "FFFFFF", onHi: "alert" },
|
|
311
311
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
title: Inovação & Design
|
|
2
|
-
author:
|
|
2
|
+
author: Estúdio Prata
|
|
3
3
|
theme: prata
|
|
4
4
|
duration: 12
|
|
5
5
|
slides:
|
|
6
6
|
- layout: cover
|
|
7
|
-
kicker: Apresentação Especial ·
|
|
7
|
+
kicker: Apresentação Especial · Lançamento
|
|
8
8
|
title: Simplicidade com ==Impacto==.
|
|
9
9
|
subtitle: A estética do respiro, elegância e precisão visual
|
|
10
10
|
author: Design Team
|
|
@@ -79,12 +79,12 @@ slides:
|
|
|
79
79
|
title: "Padrão de Mercado"
|
|
80
80
|
text: "A referência definitiva para profissionais sérios."
|
|
81
81
|
rating: 5
|
|
82
|
-
ratingLabel: "5.0
|
|
83
|
-
tags: ["
|
|
82
|
+
ratingLabel: "5.0 Pro"
|
|
83
|
+
tags: ["Lançamento", "Prata", "Clean"]
|
|
84
84
|
|
|
85
85
|
- layout: end
|
|
86
|
-
title:
|
|
86
|
+
title: Simples assim.
|
|
87
87
|
subtitle: Obrigado.
|
|
88
88
|
contacts:
|
|
89
|
-
-
|
|
89
|
+
- design.org
|
|
90
90
|
- contato@design.org
|