g360-cli 1.15.8 → 1.17.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 +84 -4
- package/package.json +2 -1
- package/src/assets/pptx/design-tokens.js +124 -0
- package/src/assets/pptx/layouts/base.js +441 -0
- package/src/assets/pptx/layouts/index.js +19 -0
- package/src/assets/pptx/scripts/analyze-app.js +358 -0
- package/src/assets/pptx/scripts/image-size.js +56 -0
- package/src/assets/pptx/templates/app-manual.js +397 -0
- package/src/assets/pptx/themes/base.js +8 -0
- package/src/assets/pptx/themes/cipsa.js +4 -0
- package/src/assets/pptx/themes/g360.js +4 -0
- package/src/assets/pptx/themes/index.js +87 -0
- package/src/cli.js +11 -0
- package/src/commands/pptx.js +91 -0
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ CLI tool para el ecosistema G360 que permite inicializar proyectos con estructur
|
|
|
97
97
|
|
|
98
98
|
## Versión
|
|
99
99
|
|
|
100
|
-
**Current: v1.
|
|
100
|
+
**Current: v1.17.0** — [Ver en npm](https://www.npmjs.com/package/g360-cli)
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
@@ -1151,7 +1151,7 @@ g360-cli/
|
|
|
1151
1151
|
|---------|-------------|
|
|
1152
1152
|
| `npm run build` | Build portable con pkg (g360.exe) |
|
|
1153
1153
|
| `npm run build:portable` | Especificar target node18-win-x64 |
|
|
1154
|
-
|
|
1154
|
+
| `npm test` | Ejecutar tests con Vitest (57 tests, 9 suites) |
|
|
1155
1155
|
| `npm run prepublishOnly` | Validación antes de publicar en npm |
|
|
1156
1156
|
|
|
1157
1157
|
---
|
|
@@ -1159,7 +1159,7 @@ g360-cli/
|
|
|
1159
1159
|
## Testing
|
|
1160
1160
|
|
|
1161
1161
|
```bash
|
|
1162
|
-
npm test # Vitest —
|
|
1162
|
+
npm test # Vitest — 57 tests, 9 suites
|
|
1163
1163
|
npm run test:watch # Modo watch
|
|
1164
1164
|
npm run test:ui # UI interactiva
|
|
1165
1165
|
npm run test:coverage
|
|
@@ -1337,7 +1337,87 @@ g360 lint --project ./mi-proyecto
|
|
|
1337
1337
|
|
|
1338
1338
|
---
|
|
1339
1339
|
|
|
1340
|
-
|
|
1340
|
+
### `g360 pptx`
|
|
1341
|
+
|
|
1342
|
+
Genera manuales y presentaciones en PowerPoint (.pptx) desde una app G360. Detecta automáticamente el framework (Flet, SvelteKit, Lit, React, Vue), los módulos UI, las capacidades (PWA, Supabase, Excel, búsqueda) y el tema de marca. El archivo se genera **dentro del propio repo de la app**, independiente para cada proyecto.
|
|
1343
|
+
|
|
1344
|
+
```bash
|
|
1345
|
+
g360 pptx [ruta] [opciones]
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
**Opciones:**
|
|
1349
|
+
|
|
1350
|
+
| Opción | Descripción | Valor por defecto |
|
|
1351
|
+
|--------|-------------|-------------------|
|
|
1352
|
+
| `--mode <tipo>` | Modo de generacion (`manual`, `demo`, `onboarding`) | `manual` |
|
|
1353
|
+
| `--theme <nombre>` | Tema de marca (`g360`, `cipsa`, `corporate`) | auto-detectado desde `skill.json` |
|
|
1354
|
+
| `--out <archivo>` | Ruta del archivo de salida | `{repo}/{app-name}-manual.pptx` |
|
|
1355
|
+
| `--dry-run` | Solo muestra outline sin generar | `false` |
|
|
1356
|
+
|
|
1357
|
+
**Modos:**
|
|
1358
|
+
|
|
1359
|
+
| Modo | Proposito | Formato |
|
|
1360
|
+
|------|-----------|---------|
|
|
1361
|
+
| `manual` | Documentacion de usuario | 16:9 (compartir/imprimir) |
|
|
1362
|
+
| `demo` | Presentaciones comerciales | 16:9 (proyector/screens) |
|
|
1363
|
+
| `onboarding` | Guia de inicio rapido | 16:9 |
|
|
1364
|
+
|
|
1365
|
+
**Detección automática por framework:**
|
|
1366
|
+
|
|
1367
|
+
| Framework | Módulos UI | Capacidad |
|
|
1368
|
+
|-----------|-----------|-----------|
|
|
1369
|
+
| Flet | `src/ui/*.py` + `src/ui/modals/` | desde `src/app.py` (auto-refresh, search, export) |
|
|
1370
|
+
| SvelteKit | `src/routes/**/+page.svelte` (incluye subrutas dinámicas) | desde `package.json` |
|
|
1371
|
+
| Lit / React / Vue | `src/components/*.js\|ts` | desde `package.json` (supabase, PWA, exceljs, charts, tailwind) |
|
|
1372
|
+
|
|
1373
|
+
**Ejemplos:**
|
|
1374
|
+
|
|
1375
|
+
```bash
|
|
1376
|
+
# Generar manual dentro del repo de la app (theme auto-detectado)
|
|
1377
|
+
g360 pptx ../g360-ventas-pulse
|
|
1378
|
+
|
|
1379
|
+
# Manual para app CIPSA (auto-detectado desde skill.json)
|
|
1380
|
+
g360 pptx ../mi-app --mode manual
|
|
1381
|
+
|
|
1382
|
+
# Solo preview del outline
|
|
1383
|
+
g360 pptx . --dry-run
|
|
1384
|
+
|
|
1385
|
+
# Presentacion 16:9 con tema corporate
|
|
1386
|
+
g360 pptx . --mode demo --theme corporate --out demo.pptx
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
**Estructura generada (16:9 widescreen 13.33x7.5in):**
|
|
1390
|
+
|
|
1391
|
+
| # | Slide | Contenido |
|
|
1392
|
+
|---|-------|-----------|
|
|
1393
|
+
| 1 | Portada | Nombre + descripcion + tagline + version + logo |
|
|
1394
|
+
| 2 | ¿Que es? | Descripcion + tarjetas usuario/tecnico + capacidades |
|
|
1395
|
+
| 3 | Modulos | Tabla de modulos + capturas laterales |
|
|
1396
|
+
| 4 | Flujo de trabajo | Pasos numerados + capturas |
|
|
1397
|
+
| 5-N | Features | Una por modulo UI detectado |
|
|
1398
|
+
| N+1 | Flujos | Modals y workflows (PWA, busqueda en web) |
|
|
1399
|
+
| N+2 | Arquitectura | Diagrama por capas |
|
|
1400
|
+
| N+3 | Buenas practicas | Checklist de uso |
|
|
1401
|
+
| N+4 | Limites conocidos | Solo si el proyecto define `limits` |
|
|
1402
|
+
| N+5 | Resumen | Sintesis final |
|
|
1403
|
+
|
|
1404
|
+
**Screenshots:**
|
|
1405
|
+
|
|
1406
|
+
Colocar imagenes en `assets/screenshots/` para inclusion automatica con encaje proporcional (nunca distorsiona):
|
|
1407
|
+
- `dashboard.png` — vista principal
|
|
1408
|
+
- `kpi-card.png`, `modal-export.png` — componentes relevantes
|
|
1409
|
+
|
|
1410
|
+
Si no hay screenshots, se generan placeholders-guia ("CAPTURA PENDIENTE") que indican que capturar y donde guardarlo.
|
|
1411
|
+
|
|
1412
|
+
**Temas disponibles** (design tokens unificados en `design-tokens.js`):
|
|
1413
|
+
|
|
1414
|
+
- **`g360`**: Esmeralda `#10B981` sobre slate (default)
|
|
1415
|
+
- **`cipsa`**: Verde corporativo `#008F5D`, logo CIPSA
|
|
1416
|
+
- **`corporate`**: Azul marino `#1E40AF`, neutral para apps sin marca
|
|
1417
|
+
|
|
1418
|
+
---
|
|
1419
|
+
|
|
1420
|
+
## Integracion con OpenCode
|
|
1341
1421
|
|
|
1342
1422
|
g360-cli incluye integración con **OpenCode** para desarrollo asistido por IA. Esta integración permite que los agentes de IA tengan acceso a los recursos de g360-cli durante el desarrollo.
|
|
1343
1423
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "g360-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"fs-extra": "^11.3.4",
|
|
55
55
|
"inquirer": "^13.4.2",
|
|
56
56
|
"ora": "^9.4.0",
|
|
57
|
+
"pptxgenjs": "^4.0.1",
|
|
57
58
|
"winston": "^3.19.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design tokens del sistema de presentaciones G360.
|
|
3
|
+
* Fuente unica de verdad: todas las paletas, tipografia y espaciados
|
|
4
|
+
* se definen aqui y son consumidos por themes/ y layouts/.
|
|
5
|
+
*
|
|
6
|
+
* Principios:
|
|
7
|
+
* - Formato 16:9 widescreen (13.333 x 7.5 in), tipo deck corporativo.
|
|
8
|
+
* - Un acento principal + un oscuro para titulos + tintes suaves para superficies.
|
|
9
|
+
* - Ambar reservado para advertencias/limites; rojo solo para errores.
|
|
10
|
+
* - Texto principal casi-negro, secundario gris slate.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Paletas de marca disponibles */
|
|
14
|
+
export const PALETTES = {
|
|
15
|
+
/** G360 — esmeralda tech sobre slate neutro */
|
|
16
|
+
g360: {
|
|
17
|
+
accent: '10B981',
|
|
18
|
+
accentDark: '047857',
|
|
19
|
+
accentSoft: 'E7F6F0',
|
|
20
|
+
onAccent: 'FFFFFF',
|
|
21
|
+
text: '0F172A',
|
|
22
|
+
textMuted: '64748B',
|
|
23
|
+
bg: 'FFFFFF',
|
|
24
|
+
surface: 'F3F5F9',
|
|
25
|
+
border: 'E3E8F0',
|
|
26
|
+
warning: 'B45309',
|
|
27
|
+
warningSoft: 'FDF3E3',
|
|
28
|
+
info: '3B82F6',
|
|
29
|
+
violet: '8B5CF6',
|
|
30
|
+
success: '059669',
|
|
31
|
+
danger: 'DC2626',
|
|
32
|
+
coverTitle: '064E3B',
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/** CIPSA — verde corporativo (alineado al manual de ventas-pulse) */
|
|
36
|
+
cipsa: {
|
|
37
|
+
accent: '008F5D',
|
|
38
|
+
accentDark: '0B6B47',
|
|
39
|
+
accentSoft: 'E3F0EB',
|
|
40
|
+
onAccent: 'FFFFFF',
|
|
41
|
+
text: '1F2937',
|
|
42
|
+
textMuted: '64748B',
|
|
43
|
+
bg: 'FFFFFF',
|
|
44
|
+
surface: 'F8FAF9',
|
|
45
|
+
border: 'C8E6D8',
|
|
46
|
+
warning: 'B45309',
|
|
47
|
+
warningSoft: 'FBF1E4',
|
|
48
|
+
info: '0369A1',
|
|
49
|
+
violet: '6D28D9',
|
|
50
|
+
success: '008F5D',
|
|
51
|
+
danger: 'DC2626',
|
|
52
|
+
coverTitle: '0B6B47',
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/** Corporate — azul marino neutral para apps sin marca definida */
|
|
56
|
+
corporate: {
|
|
57
|
+
accent: '1E40AF',
|
|
58
|
+
accentDark: '172554',
|
|
59
|
+
accentSoft: 'E7ECF9',
|
|
60
|
+
onAccent: 'FFFFFF',
|
|
61
|
+
text: '0F172A',
|
|
62
|
+
textMuted: '64748B',
|
|
63
|
+
bg: 'FFFFFF',
|
|
64
|
+
surface: 'F4F6FB',
|
|
65
|
+
border: 'D6DEF0',
|
|
66
|
+
warning: 'B45309',
|
|
67
|
+
warningSoft: 'FDF3E3',
|
|
68
|
+
info: '0E7490',
|
|
69
|
+
violet: '6D28D9',
|
|
70
|
+
success: '047857',
|
|
71
|
+
danger: 'DC2626',
|
|
72
|
+
coverTitle: '172554',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Tipografia: pila de fuentes y escala de puntos */
|
|
77
|
+
export const TYPO = {
|
|
78
|
+
font: 'Inter',
|
|
79
|
+
fallback: 'Arial',
|
|
80
|
+
sizes: {
|
|
81
|
+
coverTitle: 44,
|
|
82
|
+
coverSubtitle: 16,
|
|
83
|
+
coverMeta: 12,
|
|
84
|
+
sectionTitle: 21,
|
|
85
|
+
cardTitle: 12,
|
|
86
|
+
cardBody: 9.5,
|
|
87
|
+
body: 11.5,
|
|
88
|
+
bodySmall: 10,
|
|
89
|
+
caption: 9.5,
|
|
90
|
+
footer: 8,
|
|
91
|
+
kpiValue: 28,
|
|
92
|
+
kpiLabel: 11,
|
|
93
|
+
stepNumber: 12,
|
|
94
|
+
stepTitle: 11.5,
|
|
95
|
+
stepBody: 10.5,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Espaciados y metricas del lienzo 16:9 */
|
|
100
|
+
export const SPACING = {
|
|
101
|
+
page: { width: 13.333, height: 7.5 },
|
|
102
|
+
marginX: 0.6,
|
|
103
|
+
get contentWidth() {
|
|
104
|
+
return this.page.width - this.marginX * 2;
|
|
105
|
+
},
|
|
106
|
+
sectionTitleY: 0.3,
|
|
107
|
+
sectionRuleY: 0.85,
|
|
108
|
+
contentTopY: 1.1,
|
|
109
|
+
footerY: 7.12,
|
|
110
|
+
cardGap: 0.2,
|
|
111
|
+
radius: 0.06,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Layout por defecto registrado en pptxgenjs */
|
|
115
|
+
export const DEFAULT_LAYOUT = 'LAYOUT_16x9_G360';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resuelve una paleta por nombre con fallback seguro.
|
|
119
|
+
* @param {string} name — 'g360' | 'cipsa' | 'corporate'
|
|
120
|
+
* @returns {object} paleta
|
|
121
|
+
*/
|
|
122
|
+
export function resolvePalette(name) {
|
|
123
|
+
return PALETTES[name] || PALETTES.g360;
|
|
124
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layouts reutilizables para slides G360 (formato 16:9 widescreen).
|
|
3
|
+
* Componentes portados y alineados al manual de ventas-pulse:
|
|
4
|
+
* portada con banda, encabezado de seccion con regla, tarjetas suaves,
|
|
5
|
+
* marco de captura tipo telefono con encaje proporcional, flujo numerado,
|
|
6
|
+
* limites con barra ambar, checklist con checks y KPIs.
|
|
7
|
+
*
|
|
8
|
+
* Cada layout recibe (slide, data, theme) y retorna el slide modificado.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import { getImageSize } from '../scripts/image-size.js';
|
|
12
|
+
|
|
13
|
+
function baseTextOpts(theme) {
|
|
14
|
+
return { fontFace: theme.typo?.font || 'Inter' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bloque de texto multi-parrafo.
|
|
19
|
+
* paragraphs: [{ text, size, bold, color, align, spaceAfter }]
|
|
20
|
+
*/
|
|
21
|
+
export function textBlock(slide, { x, y, w, h = 0.5, paragraphs }, theme) {
|
|
22
|
+
const opts = baseTextOpts(theme);
|
|
23
|
+
const runs = paragraphs.map((p, i) => ({
|
|
24
|
+
text: p.text,
|
|
25
|
+
options: {
|
|
26
|
+
fontSize: p.size ?? theme.typo.sizes.body,
|
|
27
|
+
bold: p.bold ?? false,
|
|
28
|
+
color: p.color ?? theme.colors.text,
|
|
29
|
+
align: p.align ?? 'left',
|
|
30
|
+
paraSpaceAfter: p.spaceAfter ?? 6,
|
|
31
|
+
breakLine: true,
|
|
32
|
+
...opts,
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
slide.addText(runs, { x, y, w, h, ...opts });
|
|
36
|
+
return slide;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Encabezado de seccion + footer con paginado (estilo ventas-pulse).
|
|
41
|
+
*/
|
|
42
|
+
export function addSectionHeader(slide, title, theme, pageNum = null, total = null) {
|
|
43
|
+
const { colors, typo, spacing } = theme;
|
|
44
|
+
slide.addText(title, {
|
|
45
|
+
x: spacing.marginX, y: spacing.sectionTitleY,
|
|
46
|
+
w: spacing.contentWidth, h: 0.5,
|
|
47
|
+
fontSize: typo.sizes.sectionTitle, bold: true,
|
|
48
|
+
color: colors.accentDark, ...baseTextOpts(theme),
|
|
49
|
+
});
|
|
50
|
+
slide.addShape('rect', {
|
|
51
|
+
x: spacing.marginX, y: spacing.sectionRuleY,
|
|
52
|
+
w: spacing.contentWidth, h: 0.035,
|
|
53
|
+
fill: { color: colors.accent }, line: { type: 'none' },
|
|
54
|
+
});
|
|
55
|
+
if (pageNum !== null) addFooter(slide, theme, pageNum, total);
|
|
56
|
+
return slide;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Footer "powered by G360 · n/total".
|
|
61
|
+
*/
|
|
62
|
+
export function addFooter(slide, theme, slideNum, totalSlides) {
|
|
63
|
+
const { colors, typo, spacing } = theme;
|
|
64
|
+
slide.addText(`powered by G360 · ${slideNum}/${totalSlides}`, {
|
|
65
|
+
x: spacing.page.width - 3.2, y: spacing.footerY,
|
|
66
|
+
w: 3.0, h: 0.28,
|
|
67
|
+
fontSize: typo.sizes.footer, color: colors.textMuted,
|
|
68
|
+
align: 'right', ...baseTextOpts(theme),
|
|
69
|
+
});
|
|
70
|
+
return slide;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Tarjeta suave con titulo y cuerpo (componente tarjeta de ventas-pulse).
|
|
75
|
+
*/
|
|
76
|
+
export function card(slide, { x, y, w, h, title, body = [], titleColor = null }, theme) {
|
|
77
|
+
const { colors, typo } = theme;
|
|
78
|
+
slide.addShape('roundRect', {
|
|
79
|
+
x, y, w, h, rectRadius: theme.spacing.radius,
|
|
80
|
+
fill: { color: colors.accentSoft },
|
|
81
|
+
line: { color: colors.accent, width: 1 },
|
|
82
|
+
});
|
|
83
|
+
const runs = [
|
|
84
|
+
{
|
|
85
|
+
text: title,
|
|
86
|
+
options: {
|
|
87
|
+
fontSize: typo.sizes.cardTitle, bold: true,
|
|
88
|
+
color: titleColor ?? colors.accentDark,
|
|
89
|
+
paraSpaceAfter: 3, breakLine: true,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
...body.map((line) => ({
|
|
93
|
+
text: line,
|
|
94
|
+
options: {
|
|
95
|
+
fontSize: typo.sizes.cardBody,
|
|
96
|
+
color: colors.text,
|
|
97
|
+
paraSpaceAfter: 2, breakLine: true,
|
|
98
|
+
},
|
|
99
|
+
})),
|
|
100
|
+
];
|
|
101
|
+
slide.addText(runs, {
|
|
102
|
+
x: x + 0.15, y: y + 0.08, w: w - 0.3, h: h - 0.16,
|
|
103
|
+
valign: 'top', ...baseTextOpts(theme),
|
|
104
|
+
});
|
|
105
|
+
return slide;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Marco de captura estilo telefono. Coloca la imagen con encaje
|
|
110
|
+
* proporcional (contain, nunca distorsiona); si no existe el archivo,
|
|
111
|
+
* dibuja un placeholder punteado con la instruccion de captura.
|
|
112
|
+
*/
|
|
113
|
+
export function phoneShot(slide, { x, y, w, h, imagePath, label }, theme) {
|
|
114
|
+
const { colors, typo } = theme;
|
|
115
|
+
// Marco exterior
|
|
116
|
+
slide.addShape('roundRect', {
|
|
117
|
+
x: x - 0.07, y: y - 0.07, w: w + 0.14, h: h + 0.14,
|
|
118
|
+
rectRadius: theme.spacing.radius,
|
|
119
|
+
fill: { color: colors.text },
|
|
120
|
+
line: { color: colors.accentDark, width: 1.75 },
|
|
121
|
+
});
|
|
122
|
+
const exists = imagePath && fs.existsSync(imagePath);
|
|
123
|
+
if (exists) {
|
|
124
|
+
const dim = getImageSize(imagePath);
|
|
125
|
+
let dw = w, dh = h;
|
|
126
|
+
if (dim && dim.width && dim.height) {
|
|
127
|
+
const ir = dim.width / dim.height;
|
|
128
|
+
const fr = w / h;
|
|
129
|
+
if (ir > fr) { dh = w / ir; } else { dw = h * ir; }
|
|
130
|
+
}
|
|
131
|
+
slide.addImage({
|
|
132
|
+
path: imagePath,
|
|
133
|
+
x: x + (w - dw) / 2,
|
|
134
|
+
y: y + (h - dh) / 2,
|
|
135
|
+
w: dw, h: dh,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
slide.addShape('roundRect', {
|
|
139
|
+
x, y, w, h, rectRadius: theme.spacing.radius,
|
|
140
|
+
fill: { color: colors.surface },
|
|
141
|
+
line: { color: colors.accent, width: 1, dashType: 'dash' },
|
|
142
|
+
});
|
|
143
|
+
slide.addText(
|
|
144
|
+
[
|
|
145
|
+
{ text: 'CAPTURA PENDIENTE', options: { fontSize: typo.sizes.cardBody, bold: true, color: colors.textMuted, paraSpaceAfter: 4, breakLine: true } },
|
|
146
|
+
{ text: label || 'Screenshot de la app', options: { fontSize: typo.sizes.cardBody, color: colors.text, paraSpaceAfter: 4, breakLine: true } },
|
|
147
|
+
...(imagePath
|
|
148
|
+
? [{ text: `Guardar como: ${imagePath.split(/[\\/]/).slice(-2).join('/')}`, options: { fontSize: 8, color: colors.textMuted, breakLine: true } }]
|
|
149
|
+
: []),
|
|
150
|
+
],
|
|
151
|
+
{ x: x + 0.12, y, w: w - 0.24, h, align: 'center', valign: 'middle', ...baseTextOpts(theme) },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return slide;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Slide de feature: descripcion + bullets.
|
|
159
|
+
*/
|
|
160
|
+
export function featureSlide(slide, data, theme) {
|
|
161
|
+
const { colors, typo, spacing } = theme;
|
|
162
|
+
slide.addText(data.title || 'Funcionalidad', {
|
|
163
|
+
x: spacing.marginX, y: 1.05, w: spacing.contentWidth, h: 0.6,
|
|
164
|
+
fontSize: 15, bold: true, color: colors.text, ...baseTextOpts(theme),
|
|
165
|
+
});
|
|
166
|
+
if (data.description) {
|
|
167
|
+
slide.addText(data.description, {
|
|
168
|
+
x: spacing.marginX, y: 1.65, w: spacing.contentWidth, h: 1.1,
|
|
169
|
+
fontSize: typo.sizes.body, color: colors.text, valign: 'top', ...baseTextOpts(theme),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (Array.isArray(data.bullets) && data.bullets.length > 0) {
|
|
173
|
+
const runs = data.bullets.map((b) => ({
|
|
174
|
+
text: b,
|
|
175
|
+
options: {
|
|
176
|
+
bullet: { code: '2022', indent: 12 },
|
|
177
|
+
fontSize: typo.sizes.bodySmall, color: colors.text,
|
|
178
|
+
paraSpaceAfter: 5, breakLine: true,
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
slide.addText(runs, {
|
|
182
|
+
x: spacing.marginX, y: 2.8, w: spacing.contentWidth, h: 3.5,
|
|
183
|
+
valign: 'top', ...baseTextOpts(theme),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return slide;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Flujo de trabajo: pasos con circulo numerado + titulo y descripcion en linea.
|
|
191
|
+
*/
|
|
192
|
+
export function workflowSlide(slide, data, theme) {
|
|
193
|
+
const { colors, typo, spacing } = theme;
|
|
194
|
+
const steps = data.steps || [];
|
|
195
|
+
const usableH = spacing.footerY - spacing.contentTopY - 0.15;
|
|
196
|
+
const rowH = Math.min(1.0, steps.length > 0 ? usableH / steps.length : 1.0);
|
|
197
|
+
let y = spacing.contentTopY;
|
|
198
|
+
steps.forEach((step, i) => {
|
|
199
|
+
slide.addShape('ellipse', {
|
|
200
|
+
x: spacing.marginX, y: y + 0.03, w: 0.44, h: 0.44,
|
|
201
|
+
fill: { color: colors.accent }, line: { type: 'none' },
|
|
202
|
+
});
|
|
203
|
+
slide.addText(String(i + 1), {
|
|
204
|
+
x: spacing.marginX, y: y + 0.03, w: 0.44, h: 0.44,
|
|
205
|
+
fontSize: typo.sizes.stepNumber, bold: true, color: colors.onAccent,
|
|
206
|
+
align: 'center', valign: 'middle', ...baseTextOpts(theme),
|
|
207
|
+
});
|
|
208
|
+
slide.addText(
|
|
209
|
+
[
|
|
210
|
+
{ text: `${step.title || ''} `, options: { fontSize: typo.sizes.stepTitle, bold: true, color: colors.accentDark, breakLine: false } },
|
|
211
|
+
{ text: step.desc || '', options: { fontSize: typo.sizes.stepBody, color: colors.text, breakLine: true } },
|
|
212
|
+
],
|
|
213
|
+
{ x: spacing.marginX + 0.65, y, w: data.textWidth ?? spacing.contentWidth - 0.65, h: rowH, valign: 'middle', ...baseTextOpts(theme) },
|
|
214
|
+
);
|
|
215
|
+
y += rowH;
|
|
216
|
+
});
|
|
217
|
+
return slide;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Limites conocidos: items con barra ambar (advertencias).
|
|
222
|
+
* items: [{ title, desc }]
|
|
223
|
+
*/
|
|
224
|
+
export function limitsSlide(slide, data, theme) {
|
|
225
|
+
const { colors, typo, spacing } = theme;
|
|
226
|
+
const items = data.items || [];
|
|
227
|
+
const cols = 2;
|
|
228
|
+
const colW = (spacing.contentWidth - spacing.cardGap) / cols;
|
|
229
|
+
const rowH = 1.75;
|
|
230
|
+
items.forEach((item, i) => {
|
|
231
|
+
const col = i % cols;
|
|
232
|
+
const row = Math.floor(i / cols);
|
|
233
|
+
const x = spacing.marginX + col * (colW + spacing.cardGap);
|
|
234
|
+
const y = spacing.contentTopY + row * rowH;
|
|
235
|
+
slide.addShape('rect', {
|
|
236
|
+
x, y: y + 0.05, w: 0.07, h: rowH - 0.25,
|
|
237
|
+
fill: { color: colors.warning }, line: { type: 'none' },
|
|
238
|
+
});
|
|
239
|
+
slide.addText(
|
|
240
|
+
[
|
|
241
|
+
{ text: item.title, options: { fontSize: 11.5, bold: true, color: colors.warning, paraSpaceAfter: 3, breakLine: true } },
|
|
242
|
+
{ text: item.desc, options: { fontSize: typo.sizes.bodySmall, color: colors.text, breakLine: true } },
|
|
243
|
+
],
|
|
244
|
+
{ x: x + 0.25, y, w: colW - 0.25, h: rowH - 0.1, valign: 'top', ...baseTextOpts(theme) },
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
return slide;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Checklist con checks verdes (buenas practicas / resumen).
|
|
252
|
+
*/
|
|
253
|
+
export function checklistSlide(slide, data, theme) {
|
|
254
|
+
const { colors, typo, spacing } = theme;
|
|
255
|
+
const items = data.items || [];
|
|
256
|
+
const startY = data.startY ?? spacing.contentTopY;
|
|
257
|
+
const rowH = data.rowH ?? 0.56;
|
|
258
|
+
items.forEach((item, i) => {
|
|
259
|
+
const y = startY + i * rowH;
|
|
260
|
+
const checked = item.checked !== false;
|
|
261
|
+
const runs = [
|
|
262
|
+
{
|
|
263
|
+
text: checked ? '✓ ' : '○ ',
|
|
264
|
+
options: { fontSize: typo.sizes.body + 0.5, bold: true, color: checked ? colors.accent : colors.textMuted, breakLine: false },
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
text: typeof item === 'string' ? item : item.text || '',
|
|
268
|
+
options: { fontSize: typo.sizes.body + 0.5, color: colors.text, breakLine: true },
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
slide.addText(runs, {
|
|
272
|
+
x: 1.0, y, w: spacing.contentWidth - 0.4, h: rowH, valign: 'middle', ...baseTextOpts(theme),
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return slide;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Tabla de modulos: filas alternadas con nombre, ruta y descripcion.
|
|
280
|
+
* rows: [{ name, route, desc }]
|
|
281
|
+
*/
|
|
282
|
+
export function modulesTable(slide, { rows, y = null, w = null }, theme) {
|
|
283
|
+
const { colors, typo, spacing } = theme;
|
|
284
|
+
const width = w ?? spacing.contentWidth - 4.35; // deja sitio a capturas laterales
|
|
285
|
+
let yy = y ?? spacing.contentTopY;
|
|
286
|
+
const rowH = 0.98;
|
|
287
|
+
rows.forEach((row, i) => {
|
|
288
|
+
slide.addShape('roundRect', {
|
|
289
|
+
x: spacing.marginX, y: yy, w: width, h: rowH, rectRadius: theme.spacing.radius,
|
|
290
|
+
fill: { color: i % 2 === 0 ? colors.accentSoft : colors.bg },
|
|
291
|
+
line: { color: colors.accent, width: 0.75 },
|
|
292
|
+
});
|
|
293
|
+
slide.addText(row.name, {
|
|
294
|
+
x: spacing.marginX + 0.2, y: yy + 0.1, w: 1.55, h: 0.4,
|
|
295
|
+
fontSize: 12.5, bold: true, color: colors.accentDark, ...baseTextOpts(theme),
|
|
296
|
+
});
|
|
297
|
+
if (row.route) {
|
|
298
|
+
slide.addText(row.route, {
|
|
299
|
+
x: spacing.marginX + 1.75, y: yy + 0.13, w: 1.8, h: 0.4,
|
|
300
|
+
fontSize: typo.sizes.cardBody, color: colors.textMuted, ...baseTextOpts(theme),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
slide.addText(row.desc, {
|
|
304
|
+
x: spacing.marginX + 3.6, y: yy + 0.09, w: width - 3.8, h: rowH - 0.13,
|
|
305
|
+
fontSize: typo.sizes.cardBody, color: colors.text, valign: 'top', ...baseTextOpts(theme),
|
|
306
|
+
});
|
|
307
|
+
yy += rowH + 0.14;
|
|
308
|
+
});
|
|
309
|
+
return yy;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* KPI cards (dashboard).
|
|
314
|
+
* kpis: [{ label, value, sub, color }]
|
|
315
|
+
*/
|
|
316
|
+
export function kpiSlide(slide, data, theme) {
|
|
317
|
+
const { colors, typo, spacing } = theme;
|
|
318
|
+
const kpis = data.kpis || [];
|
|
319
|
+
const cols = Math.min(kpis.length, 4) || 1;
|
|
320
|
+
const colW = (spacing.contentWidth - spacing.cardGap * (cols - 1)) / cols;
|
|
321
|
+
kpis.forEach((kpi, i) => {
|
|
322
|
+
const x = spacing.marginX + i * (colW + spacing.cardGap);
|
|
323
|
+
const color = kpi.color || colors.accent;
|
|
324
|
+
slide.addShape('roundRect', {
|
|
325
|
+
x, y: 1.2, w: colW, h: 2.5, rectRadius: theme.spacing.radius,
|
|
326
|
+
fill: { color: colors.surface }, line: { color, width: 1 },
|
|
327
|
+
});
|
|
328
|
+
slide.addText(kpi.value || '—', {
|
|
329
|
+
x, y: 1.35, w: colW, h: 0.9,
|
|
330
|
+
fontSize: typo.sizes.kpiValue, bold: true, color,
|
|
331
|
+
align: 'center', fontFace: 'JetBrains Mono, Consolas, monospace',
|
|
332
|
+
});
|
|
333
|
+
slide.addText(kpi.label || '', {
|
|
334
|
+
x, y: 2.3, w: colW, h: 0.35,
|
|
335
|
+
fontSize: typo.sizes.kpiLabel, color: colors.textMuted, align: 'center', ...baseTextOpts(theme),
|
|
336
|
+
});
|
|
337
|
+
if (kpi.sub) {
|
|
338
|
+
slide.addText(kpi.sub, {
|
|
339
|
+
x, y: 2.65, w: colW, h: 0.3,
|
|
340
|
+
fontSize: 10, color: colors.textMuted, align: 'center', ...baseTextOpts(theme),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
return slide;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Arquitectura: capas apiladas con color por rol.
|
|
349
|
+
* layers: [{ name, desc, color }]
|
|
350
|
+
*/
|
|
351
|
+
export function architectureSlide(slide, data, theme) {
|
|
352
|
+
const { colors, typo, spacing } = theme;
|
|
353
|
+
const layers = data.layers || [];
|
|
354
|
+
const layerH = 1.15;
|
|
355
|
+
let y = spacing.contentTopY + 0.1;
|
|
356
|
+
layers.forEach((layer, i) => {
|
|
357
|
+
const color = layer.color || colors.accent;
|
|
358
|
+
slide.addShape('roundRect', {
|
|
359
|
+
x: spacing.marginX, y, w: spacing.contentWidth, h: layerH,
|
|
360
|
+
rectRadius: theme.spacing.radius,
|
|
361
|
+
fill: { color: colors.surface }, line: { color, width: 1.5 },
|
|
362
|
+
});
|
|
363
|
+
slide.addText(layer.name, {
|
|
364
|
+
x: spacing.marginX + 0.25, y: y + 0.1, w: 2.5, h: layerH - 0.2,
|
|
365
|
+
fontSize: 15, bold: true, color, valign: 'middle', ...baseTextOpts(theme),
|
|
366
|
+
});
|
|
367
|
+
slide.addText(layer.desc, {
|
|
368
|
+
x: spacing.marginX + 3.0, y: y + 0.1, w: spacing.contentWidth - 3.2, h: layerH - 0.2,
|
|
369
|
+
fontSize: typo.sizes.bodySmall, color: colors.text, valign: 'middle', ...baseTextOpts(theme),
|
|
370
|
+
});
|
|
371
|
+
if (i < layers.length - 1) {
|
|
372
|
+
slide.addText('▼', {
|
|
373
|
+
x: spacing.page.width / 2 - 0.2, y: y + layerH + 0.01, w: 0.4, h: 0.24,
|
|
374
|
+
fontSize: 10, color: colors.textMuted, align: 'center', ...baseTextOpts(theme),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
y += layerH + 0.32;
|
|
378
|
+
});
|
|
379
|
+
return slide;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Portada: titulo grande centrado, banda de acento, subtitulo y meta.
|
|
384
|
+
*/
|
|
385
|
+
export function coverSlide(slide, data, theme) {
|
|
386
|
+
const { colors, typo, spacing } = theme;
|
|
387
|
+
const W = spacing.page.width;
|
|
388
|
+
slide.addText(data.appName || 'Mi App G360', {
|
|
389
|
+
x: 1.0, y: 1.5, w: W - 2.0, h: 1.3,
|
|
390
|
+
fontSize: typo.sizes.coverTitle, bold: true, color: colors.coverTitle,
|
|
391
|
+
align: 'center', valign: 'middle', ...baseTextOpts(theme),
|
|
392
|
+
});
|
|
393
|
+
slide.addShape('rect', {
|
|
394
|
+
x: 0, y: 3.15, w: W, h: 0.055,
|
|
395
|
+
fill: { color: colors.accent }, line: { type: 'none' },
|
|
396
|
+
});
|
|
397
|
+
const sub = slide.addText(
|
|
398
|
+
[
|
|
399
|
+
{ text: data.description || 'Documentacion de uso y funcionalidades', options: { fontSize: typo.sizes.coverSubtitle, color: colors.text, paraSpaceAfter: 3, breakLine: true } },
|
|
400
|
+
{ text: data.tagline || '', options: { fontSize: typo.sizes.coverMeta, color: colors.textMuted, breakLine: true } },
|
|
401
|
+
],
|
|
402
|
+
{ x: 1.5, y: 3.5, w: W - 3.0, h: 1.2, align: 'center', ...baseTextOpts(theme) },
|
|
403
|
+
);
|
|
404
|
+
const meta = [
|
|
405
|
+
data.version ? `Manual de uso · v${data.version}` : 'Manual de uso',
|
|
406
|
+
'powered by G360',
|
|
407
|
+
].filter(Boolean).join(' · ');
|
|
408
|
+
slide.addText(meta, {
|
|
409
|
+
x: 1.0, y: 5.4, w: W - 2.0, h: 0.9,
|
|
410
|
+
fontSize: 13, bold: false, color: colors.textMuted,
|
|
411
|
+
align: 'center', ...baseTextOpts(theme),
|
|
412
|
+
});
|
|
413
|
+
const logo = typeof theme.logo === 'function' ? theme.logo() : null;
|
|
414
|
+
if (logo) {
|
|
415
|
+
slide.addImage({ path: logo.path, x: W / 2 - logo.w / 2, y: 6.35, w: logo.w, h: logo.h });
|
|
416
|
+
}
|
|
417
|
+
return slide;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ===== Compat: nombres anteriores usados por templates =====
|
|
421
|
+
export { card as tarjeta, phoneShot as screenshotFrame };
|
|
422
|
+
|
|
423
|
+
/** screenshotSlide de compat: titulo + descripcion + marco */
|
|
424
|
+
export function screenshotSlide(slide, data, theme) {
|
|
425
|
+
const { colors, typo, spacing } = theme;
|
|
426
|
+
slide.addText(data.title || 'Captura de Pantalla', {
|
|
427
|
+
x: spacing.marginX, y: 1.05, w: spacing.contentWidth, h: 0.6,
|
|
428
|
+
fontSize: 15, bold: true, color: colors.text, ...baseTextOpts(theme),
|
|
429
|
+
});
|
|
430
|
+
if (data.description) {
|
|
431
|
+
slide.addText(data.description, {
|
|
432
|
+
x: spacing.marginX, y: 1.65, w: spacing.contentWidth, h: 0.75,
|
|
433
|
+
fontSize: typo.sizes.bodySmall, color: colors.textMuted, valign: 'top', ...baseTextOpts(theme),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return phoneShot(slide, {
|
|
437
|
+
x: spacing.marginX + 1.2, y: 2.4, w: spacing.contentWidth - 2.4, h: 4.3,
|
|
438
|
+
imagePath: data.screenshotPath,
|
|
439
|
+
label: data.title || 'Screenshot de la app',
|
|
440
|
+
}, theme);
|
|
441
|
+
}
|