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/map-config.js
CHANGED
|
@@ -12,9 +12,12 @@ export function mapaFile(env = process.env, home = os.homedir()) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const OSM = "© colaboradores do OpenStreetMap";
|
|
15
|
+
const OFM = '<a href="https://openfreemap.org/" target="_blank" rel="noopener">OpenFreeMap</a> · © <a href="https://openmaptiles.org/" target="_blank" rel="noopener">OpenMapTiles</a> · © <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">colaboradores do OpenStreetMap</a>';
|
|
15
16
|
export const MAP_DEFAULTS = {
|
|
16
|
-
//
|
|
17
|
+
// ruas conserva o OpenStreetMap; fundos de dados são vetoriais, sem a fronteira marítima desenhada no mar
|
|
17
18
|
tiles: { url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: OSM, maxZoom: 19 },
|
|
19
|
+
claro: { url: "https://tiles.openfreemap.org/styles/positron", tipo: "vetorial", attribution: OFM, maxZoom: 20 },
|
|
20
|
+
escuro: { url: "https://tiles.openfreemap.org/styles/dark", tipo: "vetorial", attribution: OFM, maxZoom: 20 },
|
|
18
21
|
// satélite só com provedor configurado (costuma pedir chave); sem ele, o slide usa as ruas e avisa
|
|
19
22
|
satelite: null,
|
|
20
23
|
geocoder: { provedor: "nominatim", url: "https://nominatim.openstreetmap.org", porSegundo: 1 },
|
|
@@ -37,7 +40,10 @@ export function loadMapConfig(file = mapaFile(), env = process.env) {
|
|
|
37
40
|
let raw = {};
|
|
38
41
|
try { raw = JSON.parse(fs.readFileSync(file, "utf8")) || {}; } catch { raw = {}; }
|
|
39
42
|
return Object.fromEntries(Object.entries(MAP_DEFAULTS).map(([k, base]) => {
|
|
40
|
-
|
|
43
|
+
let s = service(raw[k], base);
|
|
44
|
+
// arquivo antigo com tiles próprios (ou desligados): respeita esse fundo nos três modos
|
|
45
|
+
if (["claro", "escuro"].includes(k) && !Object.hasOwn(raw, k) && Object.hasOwn(raw, "tiles") && (raw.tiles === null || raw.tiles?.url && raw.tiles.url !== MAP_DEFAULTS.tiles.url)) s = service(raw.tiles, MAP_DEFAULTS.tiles);
|
|
46
|
+
if (s && ["tiles", "claro", "escuro", "satelite"].includes(k)) s = { ...s, tipo: s.url.includes("{z}") ? "raster" : s.tipo || "raster" };
|
|
41
47
|
if (s && !s.key && s.keyEnv && env[s.keyEnv]) return [k, { ...s, key: env[s.keyEnv], keyFrom: "variável" }];
|
|
42
48
|
return [k, s];
|
|
43
49
|
}));
|
|
@@ -78,6 +84,6 @@ export function maskedMapConfig(cfg) {
|
|
|
78
84
|
// o que pode ir para dentro do HTML da apresentação: só o fundo (o navegador busca os tiles). Chave de rotas e de
|
|
79
85
|
// geocodificação nunca vai para o deck.
|
|
80
86
|
export function tilesForDeck(cfg = loadMapConfig()) {
|
|
81
|
-
const pick = (t) => (t ? { url: t.url, attribution: t.attribution || OSM, maxZoom: t.maxZoom || 19 } : null);
|
|
82
|
-
return { tiles: pick(cfg.tiles), satelite: pick(cfg.satelite) };
|
|
87
|
+
const pick = (t) => (t ? { url: t.url, tipo: t.tipo || "raster", attribution: t.attribution || OSM, maxZoom: t.maxZoom || 19 } : null);
|
|
88
|
+
return { tiles: pick(cfg.tiles), claro: pick(cfg.claro === undefined ? cfg.tiles : cfg.claro), escuro: pick(cfg.escuro === undefined ? cfg.tiles : cfg.escuro), satelite: pick(cfg.satelite) };
|
|
83
89
|
}
|
package/src/map.js
CHANGED
|
@@ -110,9 +110,10 @@ export function fromKML(xml) {
|
|
|
110
110
|
|
|
111
111
|
function fromGeoJSON(obj) {
|
|
112
112
|
if (!obj || typeof obj !== "object") return [];
|
|
113
|
-
|
|
114
|
-
if (obj.type === "
|
|
115
|
-
if (obj.type
|
|
113
|
+
// _i: a posição da forma no arquivo (o editor do Studio acha a forma clicada por ela)
|
|
114
|
+
if (obj.type === "FeatureCollection") return (obj.features || []).map((f, i) => (f && f.geometry ? { ...f, properties: { ...f.properties, _i: i } } : null)).filter(Boolean);
|
|
115
|
+
if (obj.type === "Feature") return obj.geometry ? [{ ...obj, properties: { ...obj.properties, _i: 0 } }] : [];
|
|
116
|
+
if (obj.type && obj.coordinates) return [{ type: "Feature", properties: { _i: 0 }, geometry: obj }];
|
|
116
117
|
return [];
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -125,7 +126,7 @@ function fromAreas(values, warn) {
|
|
|
125
126
|
const uf = g.uf[code], pais = g.paises[code];
|
|
126
127
|
if (!uf && !pais) { warn(`mapa: "${k}" não é uma UF (SP, RJ…) nem um país (BR, PT…)`); continue; }
|
|
127
128
|
const poly = uf || pais.poly;
|
|
128
|
-
features.push({ type: "Feature", properties: { nome: uf ? code : pais.nome, valor: v }, geometry: { type: "MultiPolygon", coordinates: poly } });
|
|
129
|
+
features.push({ type: "Feature", properties: { nome: uf ? code : pais.nome, valor: v, _cod: String(k) }, geometry: { type: "MultiPolygon", coordinates: poly } });
|
|
129
130
|
}
|
|
130
131
|
return features;
|
|
131
132
|
}
|
|
@@ -204,6 +205,10 @@ export function mapModel(s, ctx) {
|
|
|
204
205
|
const head = [...new Set(rows.flatMap((p) => Object.keys(p)))];
|
|
205
206
|
features = pointsFromRows(head, rows.map((p) => head.map((h) => p[h] ?? "")), layer, warn).features;
|
|
206
207
|
}
|
|
208
|
+
} else if (layer.geojson && typeof layer.geojson === "object") {
|
|
209
|
+
// escrito no próprio slide (desenho pequeno, demonstração); dados de verdade ficam num arquivo do projeto
|
|
210
|
+
features = fromGeoJSON(layer.geojson);
|
|
211
|
+
if (!features.length) warn(`mapa: o GeoJSON da camada "${layer.name || i + 1}" não tem formas`);
|
|
207
212
|
} else if (layer.geojson) {
|
|
208
213
|
const text = readText(layer.geojson, ctx, "GeoJSON");
|
|
209
214
|
try { features = text == null ? [] : fromGeoJSON(JSON.parse(text)); } catch { warn(`mapa: "${layer.geojson}" não é um GeoJSON válido`); }
|
|
@@ -217,14 +222,22 @@ export function mapModel(s, ctx) {
|
|
|
217
222
|
if (!features.length && layer.areas == null) warn(`mapa: a camada "${layer.name || i + 1}" ficou vazia`);
|
|
218
223
|
const step = layer.step ?? (s.build ? i + 1 : undefined);
|
|
219
224
|
layers.push({
|
|
220
|
-
name: layer.name || layer.title || `Camada ${i + 1}`, kind,
|
|
221
|
-
source: typeof layer.points === "string" ? layer.points.trim() : layer.geojson || layer.gpx || layer.kml || null, step: Number.isFinite(Number(step)) ? Number(step) : undefined,
|
|
225
|
+
name: layer.name || layer.title || `Camada ${i + 1}`, kind, index: i, // index: a camada no slide (o editor do Studio)
|
|
226
|
+
source: typeof layer.points === "string" ? layer.points.trim() : (typeof layer.geojson === "string" ? layer.geojson : null) || layer.gpx || layer.kml || null, step: Number.isFinite(Number(step)) ? Number(step) : undefined,
|
|
222
227
|
view: viewOf(layer.view), legend: layer.legend || null, prefix: layer.prefix || "", suffix: layer.suffix || "",
|
|
223
228
|
style: styleOf(features, layer, warn), data: { type: "FeatureCollection", features },
|
|
224
229
|
});
|
|
225
230
|
});
|
|
226
231
|
const basemap = BASEMAPS.includes(s.basemap) ? s.basemap : "ruas";
|
|
227
232
|
if (s.basemap && !BASEMAPS.includes(s.basemap)) warn(`mapa: fundo "${s.basemap}" não existe (use ${BASEMAPS.join(", ")})`);
|
|
233
|
+
// camadas da mesma medida (a mesma coluna ou a mesma unidade na legenda) dividem uma escala: cada uma com a sua,
|
|
234
|
+
// a Bahia (2.900) saía tão forte quanto Minas (5.100) só por ser a maior da camada dela
|
|
235
|
+
const groups = new Map();
|
|
236
|
+
for (const l of layers) if (l.style.scale) { const k = `${l.style.colorBy}|${l.legend || ""}`; (groups.get(k) || groups.set(k, []).get(k)).push(l); }
|
|
237
|
+
for (const g of groups.values()) if (g.length > 1) {
|
|
238
|
+
const scale = { min: Math.min(...g.map((l) => l.style.scale.min)), max: Math.max(...g.map((l) => l.style.scale.max)) };
|
|
239
|
+
for (const l of g) l.style.scale = scale;
|
|
240
|
+
}
|
|
228
241
|
return { basemap, view: viewOf(s.view), bounds: bboxOf(layers), layers, tiles: ctx?.mapTiles || null };
|
|
229
242
|
}
|
|
230
243
|
|
|
@@ -249,11 +262,11 @@ export function mapPreviewSVG(model, w = 1600, h = 760) {
|
|
|
249
262
|
for (const layer of model.layers) for (const f of layer.data.features) {
|
|
250
263
|
const g = f.geometry, col = color(layer, f);
|
|
251
264
|
const lines = (cs) => cs.map((l) => `M${l.map(P).join("L")}`).join("");
|
|
252
|
-
if (g.type === "Point") { const [x, y] = P(g.coordinates).split(","); out.push(`<circle cx="${x}" cy="${y}" r="
|
|
265
|
+
if (g.type === "Point") { const [x, y] = P(g.coordinates).split(","); out.push(`<circle cx="${x}" cy="${y}" r="8" fill="${col}" stroke="#fff" stroke-width="3"/>`); }
|
|
253
266
|
else if (g.type === "LineString") out.push(`<path d="${lines([g.coordinates])}" fill="none" stroke="${col}" stroke-width="5"/>`);
|
|
254
267
|
else if (g.type === "MultiLineString") out.push(`<path d="${lines(g.coordinates)}" fill="none" stroke="${col}" stroke-width="5"/>`);
|
|
255
268
|
else if (g.type === "Polygon") out.push(`<path d="${lines(g.coordinates)}Z" fill="${col}" fill-opacity=".35" stroke="${col}" stroke-width="2"/>`);
|
|
256
|
-
else if (g.type === "MultiPolygon") out.push(`<path d="${g.coordinates.map((p) => lines(p) + "Z").join("")}" fill="${col}" fill-opacity=".35" stroke="${col}" stroke-width="2"/>`);
|
|
269
|
+
else if (g.type === "MultiPolygon") out.push(`<path d="${g.coordinates.map((p) => lines(p) + "Z").join("")}" fill="${col}" fill-opacity=".35" stroke="${col}" stroke-width="${f.properties?._faixa ? 0 : 2}"/>`);
|
|
257
270
|
}
|
|
258
271
|
return `<svg class="map-preview" viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet" aria-hidden="true">${out.join("")}</svg>`;
|
|
259
272
|
}
|
package/src/mcp/server.js
CHANGED
|
@@ -129,6 +129,18 @@ const TOOLS = [
|
|
|
129
129
|
required: ["query"],
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
name: "sagadeck_ia_check",
|
|
134
|
+
description: "Testa a IA configurada nesta máquina, sem tela: texto, visão (o modelo precisa dizer as cores de uma imagem), geração de imagem, pesquisa na web e vídeo, cada um com uma chamada de verdade, em paralelo. Rode antes de usar recursos de IA. Status por recurso: ok, falhou, nao_configurado ou sem_prova (vídeo sem gerar). Vídeo só é gerado de verdade com video_real: true (é cobrado).",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
so: { type: "array", items: { type: "string", enum: ["texto", "visao", "imagem", "pesquisa", "video"] }, description: "Só estes recursos (padrão: todos)" },
|
|
139
|
+
rapido: { type: "boolean", description: "Pula geração de imagem e vídeo (os lentos e cobrados)" },
|
|
140
|
+
video_real: { type: "boolean", description: "Gera um clipe curto de verdade e confere o MP4 (cobrado; leva minutos)" },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
132
144
|
{
|
|
133
145
|
name: "sagadeck_launch_studio",
|
|
134
146
|
description: "Inicia o SagaDeck Studio (página web estilo PowerPoint com canvas visual, edição WYSIWYG e chat lateral com IA).",
|
|
@@ -221,6 +233,11 @@ export function runMCPServer() {
|
|
|
221
233
|
|
|
222
234
|
async function handleToolCall(name, args) {
|
|
223
235
|
switch (name) {
|
|
236
|
+
case "sagadeck_ia_check": {
|
|
237
|
+
const { iaCheck } = await import("../ai/ia-check.js");
|
|
238
|
+
return iaCheck({ only: args.so, rapido: !!args.rapido, videoReal: !!args.video_real });
|
|
239
|
+
}
|
|
240
|
+
|
|
224
241
|
case "sagadeck_read_deck": {
|
|
225
242
|
const spec = loadSpec(path.resolve(args.path));
|
|
226
243
|
return {
|
package/src/motion.js
CHANGED
|
@@ -5,13 +5,17 @@ const number=(x,f,min,max)=>Math.min(max,Math.max(min,Number.isFinite(Number(x))
|
|
|
5
5
|
const color=(x,f)=>/^#?[0-9a-f]{6}$/i.test(x||'')?'#'+String(x).replace('#',''):f;
|
|
6
6
|
export function motionOptions(value,colors={}){
|
|
7
7
|
const o=typeof value==='string'?{type:value}:value||{};
|
|
8
|
-
if(!['wireframe','hologram','trace','requests','flow','chat','terminal'].includes(o.type))throw new Error('motion.type: wireframe, hologram, trace, requests, flow, chat ou
|
|
8
|
+
if(!['wireframe','hologram','trace','requests','flow','chat','terminal','console'].includes(o.type))throw new Error('motion.type: wireframe, hologram, trace, requests, flow, chat, terminal ou console.');
|
|
9
|
+
// console: janela de terminal com cenas (comando digitado letra a letra, depois a saída linha a linha), em ciclo
|
|
10
|
+
const con=o.type==='console';
|
|
11
|
+
const scenes=(Array.isArray(o.scenes)?o.scenes:con&&Array.isArray(o.lines)?o.lines:[]).slice(0,12).map(sc=>typeof sc==='string'?{cmd:sc,out:[]}:{cmd:String(sc?.cmd??''),out:(Array.isArray(sc?.out)?sc.out:String(sc?.out??'').split('\n')).map(String).filter(x=>x!=='').slice(0,8)}).filter(sc=>sc.cmd);
|
|
12
|
+
if(con&&!scenes.length)throw new Error('motion console: scenes: [{cmd: "comando", out: ["linha de saída", ...]}].');
|
|
9
13
|
const aliases={fg:'ink',bg:'paper',hi:'accent',em:'alert'};
|
|
10
14
|
const resolved=(v,f)=>color(colors[aliases[v]||v]||v,f);
|
|
11
15
|
const trace=(Array.isArray(o.path)&&o.path.length>=2?o.path:[[.05,.5],[.95,.5]]).slice(0,64).map(p=>[number(Array.isArray(p)?p[0]:p?.x,0,0,1),number(Array.isArray(p)?p[1]:p?.y,0,0,1)]);
|
|
12
16
|
const transparent=o.background==='transparent';
|
|
13
17
|
const layers=(Array.isArray(o.layers)&&o.layers.length?o.layers:[{shape:o.shape,x:.5,y:.5,scale:.24}]).slice(0,6).map(l=>({shape:['sphere','cube','plane','ring'].includes(l.shape)?l.shape:'sphere',x:number(l.x,.5,-.5,1.5),y:number(l.y,.5,-.5,1.5),scale:number(l.scale,.24,.03,1),speed:number(l.speed,1,.1,4),rotation:number(l.rotation,0,-10,10)}));
|
|
14
|
-
return {type:o.type,path:trace,closed:o.closed===true,pulses:Math.round(number(o.pulses,2,1,8)),lineWidth:number(o.lineWidth,1.2,.5,6),layers,opacity:number(o.opacity,.7,.05,1),lines:Array.isArray(o.lines)?o.lines.map(String):String(o.code||'').split('\n'),fontSize:number(o.fontSize,16,10,40),shape:o.shape==='cube'?'cube':'sphere',speed:number(o.speed,1,.1,8),recognition:number(o.recognition,1,0,10),thinking:number(o.thinking,.8,0,10),read:number(o.read,2,0,15),typing:number(o.typing
|
|
18
|
+
return {type:o.type,path:trace,closed:o.closed===true,pulses:Math.round(number(o.pulses,2,1,8)),lineWidth:number(o.lineWidth,1.2,.5,6),layers,opacity:number(o.opacity,.7,.05,1),lines:Array.isArray(o.lines)?o.lines.map(String):String(o.code||'').split('\n'),fontSize:number(o.fontSize,con?20:16,10,40),shape:o.shape==='cube'?'cube':'sphere',speed:number(o.speed,1,.1,8),recognition:number(o.recognition,1,0,10),thinking:number(o.thinking,.8,0,10),read:number(o.read,2,0,15),typing:number(o.typing,con?.016:.035,.005,.2),scenes,hold:number(o.hold,3.2,.5,30),caption:String(o.caption||''),user:String(o.user||''),assistant:String(o.assistant||''),background:transparent?'transparent':resolved(o.background,con?color(colors.surfaceDark||colors.surface,'#12160f'):'#111e30'),foreground:resolved(o.foreground,con?color(colors.ink,'#ece8db'):transparent?color(colors.ink,'#182642'):'#b5c9db'),accent:resolved(o.accent,con?color(colors.accent,'#f0b24a'):color(colors.alert,'#cc092f')),secondary:resolved(o.secondary,con?color(colors.c2||colors.accent,'#a9c79b'):transparent?color(colors.accent,'#3769e8'):'#aa9ccf'),muted:color(colors.muted,'#9fa89a'),line:color(colors.line,'#2b352c'),alert:color(colors.alert,'#e07a55'),manual:o.start!=='auto',finish:['next','video','hold'].includes(o.finish)?o.finish:'hold'};
|
|
15
19
|
}
|
|
16
20
|
function drawTrace(ctx,o,t,w,h){
|
|
17
21
|
const points=o.path.map(p=>[p[0]*w,p[1]*h]);if(o.closed)points.push(points[0]);
|
|
@@ -26,11 +30,21 @@ function scene(o){
|
|
|
26
30
|
const message=(kind)=>parent.postMessage({sagaMotion:kind},'*');
|
|
27
31
|
const chat=document.querySelector('.chat'),input=document.querySelector('.input'),thread=document.querySelector('.thread'),status=document.querySelector('.status');
|
|
28
32
|
function reset(){started=false;phase='idle';elapsed=0;chat.hidden=true;input.textContent='';thread.replaceChildren();status.textContent='';}
|
|
29
|
-
window.__sagaMotion=()=>({active,reduced,started,serial,phase});message('ready');window.addEventListener('message',e=>{if(e.source!==parent)return;if(e.data?.sagaMotion==='poster'){still=e.data.active!==false;recompute();if(reduced&&o.type==='terminal')document.querySelector('.feed').textContent=o.lines.join('\n');}if(e.data?.sagaMotion==='active')active=!!e.data.active;if(e.data?.sagaMotion==='reset')reset();if(e.data?.sagaMotion==='start'){reset();started=true;active=true;phase='recognize';chat.hidden=false;}});
|
|
33
|
+
window.__sagaMotion=()=>({active,reduced,started,serial,phase});message('ready');window.addEventListener('message',e=>{if(e.source!==parent)return;if(e.data?.sagaMotion==='poster'){still=e.data.active!==false;recompute();if(reduced&&o.type==='terminal')document.querySelector('.feed').textContent=o.lines.join('\n');if(reduced&&o.type==='console')consoleFull();}if(e.data?.sagaMotion==='active')active=!!e.data.active;if(e.data?.sagaMotion==='reset')reset();if(e.data?.sagaMotion==='start'){reset();started=true;active=true;phase='recognize';chat.hidden=false;}});
|
|
30
34
|
if(o.type==='chat'&&o.manual){const request=e=>{if(!active)return;if(e.type==='keydown'&&![' ','Enter','ArrowRight','PageDown'].includes(e.key))return;e.preventDefault();message('start-request');};document.addEventListener('click',request);document.addEventListener('keydown',request);}
|
|
31
35
|
if(o.type==='chat'&&!o.manual){started=true;phase='recognize';chat.hidden=false;}
|
|
32
36
|
function bubble(text,kind){const el=document.createElement('div');el.className='bubble '+kind;el.textContent=text;thread.append(el);return el;}
|
|
33
37
|
let terminalElapsed=0;const terminalText=o.lines.join('\n');
|
|
38
|
+
let ci=0,cphase='wait',ct=0,shown='';const conBody=document.querySelector('.con-body'),conNum=document.querySelector('.con-n');
|
|
39
|
+
function showConsole(cmdLen,outN){const sc=o.scenes[ci],key=ci+':'+cmdLen+':'+outN;if(!sc||key===shown)return;shown=key;const cmd=document.createElement('p');cmd.className='cmd';const pr=document.createElement('span');pr.className='pr';cmd.append(pr,document.createTextNode(sc.cmd.slice(0,cmdLen)));if(cmdLen<sc.cmd.length){const cur=document.createElement('span');cur.className='cur';cmd.append(cur);}const outs=sc.out.slice(0,outN).map((l,i)=>{const e=document.createElement('p');e.className='out'+(i===0?' first':'');const arr=document.createElement('span');arr.className='arr';e.append(arr,document.createTextNode(l));return e;});conBody.replaceChildren(cmd,...outs);conNum.textContent=String(ci+1).padStart(2,'0')+'/'+String(o.scenes.length).padStart(2,'0');}
|
|
40
|
+
function consoleFull(){ci=0;cphase='wait';ct=0;showConsole(o.scenes[0].cmd.length,o.scenes[0].out.length);}
|
|
41
|
+
function tickConsole(dt){const sc=o.scenes[ci];if(reduced){consoleFull();return;}ct+=dt;
|
|
42
|
+
if(cphase==='wait'){showConsole(0,0);if(ct>=.3){cphase='type';ct=0;}}
|
|
43
|
+
else if(cphase==='type'){const n=Math.min(sc.cmd.length,Math.floor(ct/o.typing));showConsole(n,0);if(n>=sc.cmd.length){cphase='gap';ct=0;}}
|
|
44
|
+
else if(cphase==='gap'){showConsole(sc.cmd.length,0);if(ct>=.38){cphase='out';ct=0;}}
|
|
45
|
+
else if(cphase==='out'){const n=Math.min(sc.out.length,1+Math.floor(ct/.26));showConsole(sc.cmd.length,n);if(n>=sc.out.length&&ct>=Math.max(0,sc.out.length-1)*.26){cphase='hold';ct=0;}}
|
|
46
|
+
else if(cphase==='hold'&&ct>=o.hold){ci=(ci+1)%o.scenes.length;cphase='wait';ct=0;}}
|
|
47
|
+
if(o.type==='console'){if(reduced)consoleFull();else showConsole(0,0);}
|
|
34
48
|
let bot;
|
|
35
49
|
function tickChat(dt){if(!started||phase==='done')return;elapsed+=dt;
|
|
36
50
|
if(phase==='recognize'&&elapsed>=o.recognition){phase='input';elapsed=0;}
|
|
@@ -40,7 +54,7 @@ function scene(o){
|
|
|
40
54
|
else if(phase==='read'&&elapsed>=o.read){phase='done';message('done');}}
|
|
41
55
|
function requestLine(){serial++;const ops=['GET /silent-cloud','POST /paper-moon','PUT /silver-orbit','PATCH /soft-vector','GET /tiny-comet','DELETE /ghost-frame'];const el=document.createElement('div');el.textContent=String(serial).padStart(6,'0')+' '+ops[(serial*7+Math.floor(serial/9))%ops.length].padEnd(23,' ')+' '+(serial*2654435761>>>0).toString(16)+' '+(17+serial*37%981)+'ms 200 OK';document.querySelector('.feed').append(el);while(document.querySelector('.feed').children.length>14)document.querySelector('.feed').firstChild.remove();document.querySelector('.feed').scrollTop=1e6;}
|
|
42
56
|
if(o.type==='requests'){for(let i=0;i<8;i++)requestLine();setInterval(()=>{if(active&&!document.hidden&&!reduced){requestLine();requestLine();}},Math.max(35,240/o.speed));}
|
|
43
|
-
function draw(ts){requestAnimationFrame(draw);if(!active||document.hidden){previous=ts;return;}const dt=Math.min(.1,(ts-(previous||ts))/1000);previous=ts;if(o.type==='chat'){tickChat(dt);return;}if(o.type==='terminal'){terminalElapsed+=dt*o.speed;const count=reduced?terminalText.length:Math.floor(terminalElapsed/o.typing);document.querySelector('.feed').textContent=terminalText.slice(0,count)+(count<terminalText.length?'_':'');document.querySelector('.feed').scrollTop=1e6;if(count>terminalText.length+3/o.typing)terminalElapsed=0;return;}if(o.type==='requests'||(reduced&&serial))return;serial++;
|
|
57
|
+
function draw(ts){requestAnimationFrame(draw);if(!active||document.hidden){previous=ts;return;}const dt=Math.min(.1,(ts-(previous||ts))/1000);previous=ts;if(o.type==='chat'){tickChat(dt);return;}if(o.type==='console'){tickConsole(dt*o.speed);return;}if(o.type==='terminal'){terminalElapsed+=dt*o.speed;const count=reduced?terminalText.length:Math.floor(terminalElapsed/o.typing);document.querySelector('.feed').textContent=terminalText.slice(0,count)+(count<terminalText.length?'_':'');document.querySelector('.feed').scrollTop=1e6;if(count>terminalText.length+3/o.typing)terminalElapsed=0;return;}if(o.type==='requests'||(reduced&&serial))return;serial++;
|
|
44
58
|
const w=main.clientWidth,h=main.clientHeight,dpr=devicePixelRatio||1;if(canvas.width!==Math.round(w*dpr)||canvas.height!==Math.round(h*dpr)){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);}ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);const t=reduced?0:(ts-epoch)/1000*o.speed,r=Math.min(w*.25,h*.32),cy=h*.55;if(o.type==='hologram'){drawHologram(ctx,o,t,w,h);return;}if(o.type==='trace'){drawTrace(ctx,o,t,w,h);return;}
|
|
45
59
|
function p(x,y,z){let a=t*.15+.4,xx=x*Math.cos(a)+z*Math.sin(a),zz=-x*Math.sin(a)+z*Math.cos(a),yy=y*Math.cos(.3)-zz*Math.sin(.3);const f=4/(4+zz);return[w/2+xx*r*f,cy+yy*r*f];}
|
|
46
60
|
function line(v,c){ctx.beginPath();v.forEach((q,i)=>{let a=p(...q);i?ctx.lineTo(...a):ctx.moveTo(...a);});ctx.strokeStyle=c;ctx.lineWidth=.9;ctx.stroke();}
|
|
@@ -53,6 +67,6 @@ function scene(o){
|
|
|
53
67
|
}
|
|
54
68
|
// still: o deck pediu movimento nenhum (motion: none). A cena já nasce parada e completa; antes ela só sabia pela
|
|
55
69
|
// mensagem da página, que sob carga chegava depois de o terminal começar a digitar.
|
|
56
|
-
export function motionHTML(value,colors,{still=false}={}){const o={...motionOptions(value,colors),...(still?{still:true}:{})};const css=`html,body{height:100%;background:${o.type
|
|
57
|
-
const html=`<main><canvas aria-hidden="true"></canvas><div class="caption">${esc(o.type==='chat'?'':o.caption)}</div><div class="feed" aria-hidden="true"></div><section class="chat" hidden><div class="chat-title">${esc(o.caption||'ASSISTENTE')}</div><div class="thread" aria-live="polite"></div><div class="status"></div><div class="input" aria-label="Mensagem digitada"></div></section></main>`;
|
|
70
|
+
export function motionHTML(value,colors,{still=false}={}){const o={...motionOptions(value,colors),...(still?{still:true}:{})};const css=`html,body{height:100%;background:${['chat','console'].includes(o.type)?'transparent':o.background};color:${o.foreground};overflow:hidden}main{height:100%;position:relative}canvas{position:absolute;inset:0;width:100%;height:100%}.caption{position:absolute;top:12px;left:16px;color:${o.secondary};font:11px monospace;letter-spacing:1.5px}.feed{position:absolute;top:36px;left:16px;right:16px;bottom:12px;overflow:hidden;font:${o.type==='terminal'?o.fontSize:12}px/1.5 monospace;white-space:pre}.chat{position:absolute;inset:0;border:1px solid ${o.secondary};border-radius:16px;padding:18px;background:${o.background};display:flex;flex-direction:column;gap:12px}.chat[hidden]{display:none}.thread{flex:1;overflow:auto;display:flex;flex-direction:column;gap:12px}.bubble{font:18px/1.45 system-ui;max-width:90%;padding:10px 14px;border-radius:10px;white-space:pre-wrap}.user{align-self:flex-end;background:#263a51}.assistant{align-self:flex-start}.status{font:14px system-ui;color:${o.secondary};min-height:18px}.input{border:1px solid #51617b;border-radius:9px;min-height:48px;padding:12px;font:17px system-ui;white-space:pre-wrap}.chat-title{font:12px system-ui;letter-spacing:2px;color:${o.secondary}}`+(o.type==='console'?`.caption{display:none}.con{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);max-height:100%;display:flex;flex-direction:column;border:1px solid ${o.line};border-radius:14px;overflow:hidden;background:${o.background};font-family:'IBM Plex Mono','Cascadia Mono',Consolas,'SF Mono',Menlo,monospace}.con-bar{flex:none;display:flex;align-items:center;gap:9px;height:44px;padding:0 18px;border-bottom:1px solid ${o.line}}.con-bar i{width:13px;height:13px;border-radius:50%;opacity:.72}.con-bar i:nth-child(1){background:${o.alert}}.con-bar i:nth-child(2){background:${o.accent}}.con-bar i:nth-child(3){background:${o.secondary}}.con-cap{margin-left:10px;font-size:13px;letter-spacing:2.5px;text-transform:uppercase;color:${o.muted};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.con-n{margin-left:auto;font-size:13px;color:${o.muted}}.con-body{flex:1;min-height:calc(${o.fontSize}px * 1.6 * 6 + 36px);overflow:hidden;padding:18px 22px;font-size:${o.fontSize}px;line-height:1.6}.con-body p{margin:0 0 .4em;white-space:pre-wrap;word-break:break-all}.cmd{color:${o.foreground}}.pr{display:inline-block;width:.42em;height:.42em;margin:0 .85em .12em .1em;border-top:.13em solid ${o.accent};border-right:.13em solid ${o.accent};transform:rotate(45deg)}.cur{display:inline-block;width:.55em;height:1.05em;margin-left:2px;vertical-align:-.18em;background:${o.accent};animation:bl 1s steps(1) infinite}@keyframes bl{50%{opacity:0}}.out{color:${o.muted}}.out.first{color:${o.secondary}}.arr{position:relative;display:inline-block;width:.8em;height:0;margin-right:.7em;vertical-align:.3em;border-top:.09em solid ${o.muted}}.arr::before{content:"";position:absolute;left:0;top:-.24em;width:.4em;height:.4em;border-left:.09em solid ${o.muted};border-bottom:.09em solid ${o.muted};transform:translateY(-.03em) rotate(45deg)}@media(prefers-reduced-motion:reduce){.cur{animation:none}}`:'');
|
|
71
|
+
const html=`<main><canvas aria-hidden="true"></canvas><div class="caption">${esc(o.type==='chat'?'':o.caption)}</div><div class="feed" aria-hidden="true"></div><section class="chat" hidden><div class="chat-title">${esc(o.caption||'ASSISTENTE')}</div><div class="thread" aria-live="polite"></div><div class="status"></div><div class="input" aria-label="Mensagem digitada"></div></section>${o.type==='console'?`<div class="con"><div class="con-bar"><i></i><i></i><i></i><span class="con-cap">${esc(o.caption)}</span><span class="con-n"></span></div><div class="con-body" aria-hidden="true"></div></div>`:''}</main>`;
|
|
58
72
|
const doc=playgroundDocument({html,css,javascript:`${drawHologram.toString()}\n${drawTrace.toString()}\n(${scene.toString()})(${JSON.stringify(o)});`});return `<iframe data-motion="${o.type}" data-motion-manual="${o.manual}" data-motion-finish="${o.finish}" title="${esc(o.caption||'Detalhe animado')}" sandbox="allow-scripts" style="width:100%;height:100%;border:0" srcdoc="${esc(doc)}"></iframe>`;}
|
package/src/purpose.js
CHANGED
|
@@ -12,7 +12,7 @@ export const PURPOSES = {
|
|
|
12
12
|
executiva: { label: "Executiva (antigo)", hint: "", maxWords: 40, dense: false, legacy: true },
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
const LAYOUT_WORDS = { onepage: 120, status: 90, solution: 140, calc: 90, algo: 60 }; // densos por natureza (uma página com tudo; exercício resolvido se revela aos poucos)
|
|
15
|
+
const LAYOUT_WORDS = { onepage: 120, status: 90, solution: 140, calc: 90, algo: 60, calendar: 240 }; // densos por natureza (uma página com tudo; exercício resolvido se revela aos poucos)
|
|
16
16
|
|
|
17
17
|
// Limite de palavras na tela de um slide: o do slide > o do deck > o do propósito (ou do layout, se maior) > 40
|
|
18
18
|
export function wordLimit(s = {}, spec = {}, layout = s.layout) {
|