g360-cli 1.16.0 → 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 +37 -27
- package/package.json +1 -1
- package/src/assets/pptx/design-tokens.js +124 -0
- package/src/assets/pptx/layouts/base.js +357 -155
- package/src/assets/pptx/layouts/index.js +6 -1
- package/src/assets/pptx/scripts/analyze-app.js +177 -13
- package/src/assets/pptx/scripts/image-size.js +56 -0
- package/src/assets/pptx/templates/app-manual.js +324 -202
- package/src/assets/pptx/themes/base.js +6 -61
- package/src/assets/pptx/themes/cipsa.js +2 -44
- package/src/assets/pptx/themes/g360.js +2 -30
- package/src/assets/pptx/themes/index.js +87 -2
- package/src/commands/pptx.js +9 -7
|
@@ -1,210 +1,343 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Layouts reutilizables para slides G360.
|
|
3
|
-
*
|
|
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.
|
|
4
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
|
+
}
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
|
-
*
|
|
18
|
+
* Bloque de texto multi-parrafo.
|
|
19
|
+
* paragraphs: [{ text, size, bold, color, align, spaceAfter }]
|
|
8
20
|
*/
|
|
9
|
-
export function
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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),
|
|
14
49
|
});
|
|
15
|
-
slide.
|
|
16
|
-
|
|
17
|
-
|
|
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' },
|
|
18
54
|
});
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
fill: { color: colors.accent }, line: { color: colors.accent },
|
|
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),
|
|
34
69
|
});
|
|
35
70
|
return slide;
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
/**
|
|
39
|
-
*
|
|
40
|
-
* data: { title, description, screenshotPath?, screenshotIndex? }
|
|
74
|
+
* Tarjeta suave con titulo y cuerpo (componente tarjeta de ventas-pulse).
|
|
41
75
|
*/
|
|
42
|
-
export function
|
|
43
|
-
const { colors } = theme;
|
|
44
|
-
slide.
|
|
45
|
-
|
|
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 },
|
|
46
82
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
50
136
|
});
|
|
51
|
-
}
|
|
52
|
-
// Area de imagen (80% ancho, centro)
|
|
53
|
-
const imgX = 0.5, imgY = 1.4, imgW = 9, imgH = 9.5;
|
|
54
|
-
if (data.screenshotPath) {
|
|
55
|
-
slide.addImage({ path: data.screenshotPath, x: imgX, y: imgY, w: imgW, h: imgH });
|
|
56
137
|
} else {
|
|
57
|
-
|
|
58
|
-
|
|
138
|
+
slide.addShape('roundRect', {
|
|
139
|
+
x, y, w, h, rectRadius: theme.spacing.radius,
|
|
59
140
|
fill: { color: colors.surface },
|
|
60
|
-
line: { color: colors.
|
|
61
|
-
});
|
|
62
|
-
slide.addText('📷 Insertar screenshot aquí', [imgX, imgY + 4, imgW, 1], {
|
|
63
|
-
fontSize: 16, color: colors.textLight, fontFace: 'Inter',
|
|
64
|
-
align: 'center', valign: 'middle',
|
|
141
|
+
line: { color: colors.accent, width: 1, dashType: 'dash' },
|
|
65
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
|
+
);
|
|
66
153
|
}
|
|
67
154
|
return slide;
|
|
68
155
|
}
|
|
69
156
|
|
|
70
157
|
/**
|
|
71
|
-
*
|
|
158
|
+
* Slide de feature: descripcion + bullets.
|
|
72
159
|
*/
|
|
73
160
|
export function featureSlide(slide, data, theme) {
|
|
74
|
-
const { colors } = theme;
|
|
75
|
-
slide.addText(data.title || 'Funcionalidad',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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),
|
|
86
184
|
});
|
|
87
185
|
}
|
|
88
186
|
return slide;
|
|
89
187
|
}
|
|
90
188
|
|
|
91
189
|
/**
|
|
92
|
-
*
|
|
190
|
+
* Flujo de trabajo: pasos con circulo numerado + titulo y descripcion en linea.
|
|
93
191
|
*/
|
|
94
192
|
export function workflowSlide(slide, data, theme) {
|
|
95
|
-
const { colors } = theme;
|
|
96
|
-
slide.addText(data.title || 'Flujo de trabajo', [0.5, 0.3, 9, 0.5], {
|
|
97
|
-
fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
|
|
98
|
-
});
|
|
193
|
+
const { colors, typo, spacing } = theme;
|
|
99
194
|
const steps = data.steps || [];
|
|
100
|
-
const
|
|
101
|
-
const
|
|
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;
|
|
102
198
|
steps.forEach((step, i) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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' },
|
|
106
202
|
});
|
|
107
|
-
slide.addText(
|
|
108
|
-
|
|
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),
|
|
109
207
|
});
|
|
110
|
-
slide.addText(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
118
216
|
});
|
|
119
217
|
return slide;
|
|
120
218
|
}
|
|
121
219
|
|
|
122
220
|
/**
|
|
123
|
-
*
|
|
221
|
+
* Limites conocidos: items con barra ambar (advertencias).
|
|
222
|
+
* items: [{ title, desc }]
|
|
124
223
|
*/
|
|
125
|
-
export function
|
|
126
|
-
const { colors } = theme;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
slide.addShape('rect', [0.5, y, 9, layerH], {
|
|
140
|
-
fill: { color: layer.color + '20' },
|
|
141
|
-
line: { color: layer.color, width: 1.5 },
|
|
142
|
-
});
|
|
143
|
-
slide.addText(layer.name, [0.7, y + 0.3, 2.5, 0.5], {
|
|
144
|
-
fontSize: 16, bold: true, color: layer.color, fontFace: 'Inter',
|
|
145
|
-
});
|
|
146
|
-
slide.addText(layer.desc, [3.3, y + 0.35, 6, 0.4], {
|
|
147
|
-
fontSize: 13, color: colors.text, fontFace: 'Inter',
|
|
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' },
|
|
148
238
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
fontSize:
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
);
|
|
154
246
|
});
|
|
155
247
|
return slide;
|
|
156
248
|
}
|
|
157
249
|
|
|
158
250
|
/**
|
|
159
|
-
* Checklist
|
|
251
|
+
* Checklist con checks verdes (buenas practicas / resumen).
|
|
160
252
|
*/
|
|
161
253
|
export function checklistSlide(slide, data, theme) {
|
|
162
|
-
const { colors } = theme;
|
|
163
|
-
slide.addText(data.title || 'Resumen', [0.5, 0.3, 9, 0.5], {
|
|
164
|
-
fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
|
|
165
|
-
});
|
|
254
|
+
const { colors, typo, spacing } = theme;
|
|
166
255
|
const items = data.items || [];
|
|
256
|
+
const startY = data.startY ?? spacing.contentTopY;
|
|
257
|
+
const rowH = data.rowH ?? 0.56;
|
|
167
258
|
items.forEach((item, i) => {
|
|
168
|
-
const y =
|
|
259
|
+
const y = startY + i * rowH;
|
|
169
260
|
const checked = item.checked !== false;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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),
|
|
175
273
|
});
|
|
176
274
|
});
|
|
177
275
|
return slide;
|
|
178
276
|
}
|
|
179
277
|
|
|
180
278
|
/**
|
|
181
|
-
*
|
|
279
|
+
* Tabla de modulos: filas alternadas con nombre, ruta y descripcion.
|
|
280
|
+
* rows: [{ name, route, desc }]
|
|
182
281
|
*/
|
|
183
|
-
export function
|
|
184
|
-
const { colors } = theme;
|
|
185
|
-
|
|
186
|
-
|
|
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;
|
|
187
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;
|
|
188
318
|
const kpis = data.kpis || [];
|
|
189
|
-
const cols = Math.min(kpis.length, 4);
|
|
190
|
-
const colW =
|
|
319
|
+
const cols = Math.min(kpis.length, 4) || 1;
|
|
320
|
+
const colW = (spacing.contentWidth - spacing.cardGap * (cols - 1)) / cols;
|
|
191
321
|
kpis.forEach((kpi, i) => {
|
|
192
|
-
const x =
|
|
193
|
-
const
|
|
194
|
-
slide.addShape('
|
|
195
|
-
|
|
196
|
-
|
|
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 },
|
|
197
327
|
});
|
|
198
|
-
slide.addText(kpi.value || '—',
|
|
199
|
-
|
|
200
|
-
|
|
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',
|
|
201
332
|
});
|
|
202
|
-
slide.addText(kpi.label || '',
|
|
203
|
-
|
|
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),
|
|
204
336
|
});
|
|
205
337
|
if (kpi.sub) {
|
|
206
|
-
slide.addText(kpi.sub,
|
|
207
|
-
|
|
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),
|
|
208
341
|
});
|
|
209
342
|
}
|
|
210
343
|
});
|
|
@@ -212,28 +345,97 @@ export function kpiSlide(slide, data, theme) {
|
|
|
212
345
|
}
|
|
213
346
|
|
|
214
347
|
/**
|
|
215
|
-
*
|
|
348
|
+
* Arquitectura: capas apiladas con color por rol.
|
|
349
|
+
* layers: [{ name, desc, color }]
|
|
216
350
|
*/
|
|
217
|
-
export function
|
|
218
|
-
const { colors } = theme;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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;
|
|
224
378
|
});
|
|
379
|
+
return slide;
|
|
225
380
|
}
|
|
226
381
|
|
|
227
382
|
/**
|
|
228
|
-
*
|
|
383
|
+
* Portada: titulo grande centrado, banda de acento, subtitulo y meta.
|
|
229
384
|
*/
|
|
230
|
-
export function
|
|
231
|
-
const { colors } = theme;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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' },
|
|
235
396
|
});
|
|
236
|
-
|
|
237
|
-
|
|
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),
|
|
238
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);
|
|
239
441
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Layouts
|
|
2
|
+
* Layouts re-exportados desde base.js para compatibilidad.
|
|
3
3
|
*/
|
|
4
4
|
export {
|
|
5
5
|
coverSlide,
|
|
6
6
|
screenshotSlide,
|
|
7
|
+
screenshotFrame,
|
|
7
8
|
featureSlide,
|
|
8
9
|
workflowSlide,
|
|
9
10
|
architectureSlide,
|
|
10
11
|
checklistSlide,
|
|
11
12
|
kpiSlide,
|
|
13
|
+
limitsSlide,
|
|
14
|
+
modulesTable,
|
|
15
|
+
card,
|
|
16
|
+
textBlock,
|
|
12
17
|
addFooter,
|
|
13
18
|
addSectionHeader,
|
|
14
19
|
} from './base.js';
|