@lemonppt/renderer 0.1.6 → 0.1.7

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.
@@ -3,23 +3,75 @@
3
3
  // SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  import { normalizeDeckGoal } from '@lemonppt/core';
5
5
  import PptxGenJS from 'pptxgenjs';
6
- const COLORS = {
7
- primary: '0F172A',
8
- secondary: '64748B',
9
- accent: '3B82F6',
10
- white: 'FFFFFF',
11
- light: 'F1F5F9',
12
- border: 'E2E8F0',
13
- };
14
- const DEFAULT_CHART_COLORS = ['3B82F6', '10B981', 'F59E0B', 'EF4444', '8B5CF6', 'EC4899'];
15
- const FONTS = {
16
- heading: 'Inter',
17
- body: 'Inter',
18
- mono: 'JetBrains Mono',
6
+ const THEME_CONFIGS = {
7
+ base: {
8
+ colors: {
9
+ primary: '1D1D1F',
10
+ secondary: '6E6E73',
11
+ accent: '2563EB',
12
+ white: 'FFFFFF',
13
+ light: 'F3F4F6',
14
+ border: 'E5E7EB',
15
+ surface: 'FFFFFF',
16
+ surfaceElevated: 'F9FAFB',
17
+ },
18
+ fonts: { heading: 'Inter', body: 'Inter', mono: 'JetBrains Mono' },
19
+ chartColors: ['2563EB', '10B981', 'F59E0B', 'EF4444', '8B5CF6', 'EC4899'],
20
+ },
21
+ 'dark-tech': {
22
+ colors: {
23
+ primary: 'F3F4F6',
24
+ secondary: '94A3B8',
25
+ accent: '06B6D4',
26
+ white: 'FFFFFF',
27
+ light: '1F2937',
28
+ border: '374151',
29
+ surface: '111827',
30
+ surfaceElevated: '1F2937',
31
+ },
32
+ fonts: { heading: 'Space Grotesk', body: 'Inter', mono: 'Space Mono' },
33
+ chartColors: ['06B6D4', '3B82F6', '10B981', 'F59E0B', '8B5CF6'],
34
+ },
35
+ 'warm-business': {
36
+ colors: {
37
+ primary: '3D2C24',
38
+ secondary: '8C7B70',
39
+ accent: 'E07B39',
40
+ white: 'FFFFFF',
41
+ light: 'F5EFE8',
42
+ border: 'EFE8E0',
43
+ surface: 'FFFBF7',
44
+ surfaceElevated: 'FFFFFF',
45
+ },
46
+ fonts: { heading: 'Newsreader', body: 'IBM Plex Sans', mono: 'JetBrains Mono' },
47
+ chartColors: ['E07B39', 'D97706', 'B45309', 'DC2626', 'A16207'],
48
+ },
19
49
  };
50
+ function resolveThemeConfig(theme, language) {
51
+ const config = THEME_CONFIGS[theme ?? ''] ?? THEME_CONFIGS.base;
52
+ // 中文场景优先使用 Noto 中文字体,同时保留主题字体作为备选
53
+ if (language === 'zh') {
54
+ return {
55
+ ...config,
56
+ fonts: {
57
+ heading: theme === 'warm-business' ? 'Noto Serif SC' : 'Noto Sans SC',
58
+ body: 'Noto Sans SC',
59
+ mono: 'Noto Sans SC',
60
+ },
61
+ };
62
+ }
63
+ return config;
64
+ }
65
+ let COLORS = THEME_CONFIGS.base.colors;
66
+ let FONTS = THEME_CONFIGS.base.fonts;
67
+ let CHART_COLORS = THEME_CONFIGS.base.chartColors;
20
68
  export async function exportDeckToPptx(goal, options) {
21
69
  goal = normalizeDeckGoal(goal);
22
70
  const { outFile, title = goal.title, subject, author } = options;
71
+ const theme = resolveThemeConfig(goal.theme, goal.language);
72
+ COLORS = theme.colors;
73
+ FONTS = theme.fonts;
74
+ CHART_COLORS = theme.chartColors;
23
75
  const pptx = new PptxGenJS();
24
76
  pptx.layout = 'LAYOUT_16x9';
25
77
  pptx.title = title;
@@ -29,117 +81,56 @@ export async function exportDeckToPptx(goal, options) {
29
81
  pptx.author = author;
30
82
  for (const slide of goal.slides) {
31
83
  const pptxSlide = pptx.addSlide();
32
- renderSlideToPptx(pptxSlide, slide);
84
+ pptxSlide.background = { color: COLORS.surface };
85
+ renderSlideToPptx(pptxSlide, slide, goal.theme);
33
86
  }
34
87
  await pptx.writeFile({ fileName: outFile });
35
88
  }
36
- function renderSlideToPptx(pptxSlide, slide) {
37
- switch (slide.layout) {
38
- case 'cover_v1':
39
- renderCoverV1(pptxSlide, slide.props);
40
- break;
41
- case 'table_of_contents_v1':
42
- renderTableOfContentsV1(pptxSlide, slide.props);
43
- break;
44
- case 'metric_v1':
45
- renderMetricV1(pptxSlide, slide.props);
46
- break;
47
- case 'metric_v2':
48
- renderMetricV2(pptxSlide, slide.props);
49
- break;
50
- case 'stats_v1':
51
- renderStatsV1(pptxSlide, slide.props);
52
- break;
53
- case 'chart_v1':
54
- renderChartV1(pptxSlide, slide.props);
55
- break;
56
- case 'chart_v2':
57
- renderChartV2(pptxSlide, slide.props);
58
- break;
59
- case 'content_v1':
60
- renderContentV1(pptxSlide, slide.props);
61
- break;
62
- case 'content_v2':
63
- renderContentV2(pptxSlide, slide.props);
64
- break;
65
- case 'content_v3':
66
- renderContentV3(pptxSlide, slide.props);
67
- break;
68
- case 'split_v1':
69
- renderSplitV1(pptxSlide, slide.props);
70
- break;
71
- case 'comparison_v1':
72
- renderComparisonV1(pptxSlide, slide.props);
73
- break;
74
- case 'comparison_v2':
75
- renderComparisonV2(pptxSlide, slide.props);
76
- break;
77
- case 'process_v1':
78
- renderProcessV1(pptxSlide, slide.props);
79
- break;
80
- case 'process_v2':
81
- renderProcessV2(pptxSlide, slide.props);
82
- break;
83
- case 'timeline_v1':
84
- renderTimelineV1(pptxSlide, slide.props);
85
- break;
86
- case 'roadmap_v1':
87
- renderRoadmapV1(pptxSlide, slide.props);
88
- break;
89
- case 'quote_v1':
90
- renderQuoteV1(pptxSlide, slide.props);
91
- break;
92
- case 'quote_v2':
93
- renderQuoteV2(pptxSlide, slide.props);
94
- break;
95
- case 'testimonial_v1':
96
- renderTestimonialV1(pptxSlide, slide.props);
97
- break;
98
- case 'testimonial_v2':
99
- renderTestimonialV2(pptxSlide, slide.props);
100
- break;
101
- case 'faq_v1':
102
- renderFaqV1(pptxSlide, slide.props);
103
- break;
104
- case 'feature_v1':
105
- renderFeatureV1(pptxSlide, slide.props);
106
- break;
107
- case 'team_v1':
108
- renderTeamV1(pptxSlide, slide.props);
109
- break;
110
- case 'partners_v1':
111
- renderPartnersV1(pptxSlide, slide.props);
112
- break;
113
- case 'pricing_v1':
114
- renderPricingV1(pptxSlide, slide.props);
115
- break;
116
- case 'gallery_v1':
117
- renderGalleryV1(pptxSlide, slide.props);
118
- break;
119
- case 'gallery_v2':
120
- renderGalleryV2(pptxSlide, slide.props);
121
- break;
122
- case 'image_v1':
123
- renderImageV1(pptxSlide, slide.props);
124
- break;
125
- case 'swot_v1':
126
- renderSwotV1(pptxSlide, slide.props);
127
- break;
128
- case 'pest_v1':
129
- renderPestV1(pptxSlide, slide.props);
130
- break;
131
- case 'closing_v1':
132
- renderClosingV1(pptxSlide, slide.props);
133
- break;
134
- case 'closing_v2':
135
- renderClosingV2(pptxSlide, slide.props);
136
- break;
137
- default:
138
- pptxSlide.addText(`Unknown layout: ${slide.layout}`, {
139
- x: 0.5, y: 3.5, w: 9, h: 1,
140
- fontSize: 18, color: 'EF4444', align: 'center',
141
- });
89
+ /** 通用共享版式的主题标识;旧版式中仍可能使用 'base',两者等价。 */
90
+ const UNIVERSAL_THEME = '*';
91
+ /** 与 UNIVERSAL_THEME 等价的历史主题标识,用于兼容旧版式。 */
92
+ const LEGACY_UNIVERSAL_THEME = 'base';
93
+ function normalizeUniversalTheme(theme) {
94
+ return theme === LEGACY_UNIVERSAL_THEME ? UNIVERSAL_THEME : theme;
95
+ }
96
+ /** 按 layoutId 索引的 PPTX 渲染器注册表。 */
97
+ const pptxRenderersByLayout = new Map();
98
+ /** 按 (role, theme) 二维索引的 PPTX 渲染器注册表,支持主题专属覆盖。 */
99
+ const pptxRenderersByRoleTheme = new Map();
100
+ function registerPptxLayoutRenderer(layoutId, renderFn) {
101
+ pptxRenderersByLayout.set(layoutId, renderFn);
102
+ }
103
+ /** 注册主题专属 PPTX 渲染器(按 role + theme 覆盖默认 layout 渲染器)。 */
104
+ export function registerPptxRoleRenderer(role, theme, renderFn) {
105
+ const normalizedTheme = normalizeUniversalTheme(theme);
106
+ if (!pptxRenderersByRoleTheme.has(role)) {
107
+ pptxRenderersByRoleTheme.set(role, new Map());
108
+ }
109
+ pptxRenderersByRoleTheme.get(role).set(normalizedTheme, renderFn);
110
+ }
111
+ function resolvePptxRenderer(slide, theme) {
112
+ // 1. 优先查找主题专属覆盖
113
+ if (theme) {
114
+ const byTheme = pptxRenderersByRoleTheme.get(slide.role);
115
+ const normalized = normalizeUniversalTheme(theme);
116
+ const exact = byTheme?.get(normalized);
117
+ if (exact) {
118
+ return exact;
119
+ }
120
+ }
121
+ // 2. 否则按具体 layoutId 查找通用渲染器
122
+ return pptxRenderersByLayout.get(slide.layout);
123
+ }
124
+ function renderSlideToPptx(pptxSlide, slide, theme) {
125
+ const renderer = resolvePptxRenderer(slide, theme);
126
+ if (renderer) {
127
+ renderer(pptxSlide, slide.props);
128
+ return;
142
129
  }
130
+ pptxSlide.addText(`Unknown layout: ${slide.layout}`, {
131
+ x: 0.5, y: 3.5, w: 9, h: 1,
132
+ fontSize: 18, color: 'EF4444', align: 'center',
133
+ });
143
134
  }
144
135
  // ---- Shared helpers --------------------------------------------------------
145
136
  function addKicker(slide, kicker, x = 0.8, y = 0.8) {
@@ -216,6 +207,52 @@ function renderCoverV1(slide, props) {
216
207
  });
217
208
  }
218
209
  }
210
+ function renderDarkTechCoverV1(slide, props) {
211
+ // 深色科技风背景:径向光晕 + 网格线简化示意
212
+ slide.background = { color: '0A0F1C' };
213
+ slide.addShape('rect', {
214
+ x: 0, y: 0, w: 10, h: 5.625,
215
+ fill: { color: '0A0F1C' },
216
+ });
217
+ slide.addShape('ellipse', {
218
+ x: 2.5, y: 0.5, w: 5, h: 4.5,
219
+ fill: { color: '06B6D4' },
220
+ });
221
+ if (props.image) {
222
+ try {
223
+ slide.addImage({ path: props.image, x: 0, y: 0, w: 10, h: 5.625 });
224
+ }
225
+ catch {
226
+ // ignore
227
+ }
228
+ }
229
+ if (props.kicker) {
230
+ slide.addText(props.kicker, {
231
+ x: 1, y: 1.8, w: 8, h: 0.35,
232
+ fontSize: 13, color: '06B6D4', align: 'center',
233
+ fontFace: FONTS.mono,
234
+ });
235
+ }
236
+ slide.addText(props.title, {
237
+ x: 1, y: 2.2, w: 8, h: 1.2,
238
+ fontSize: 56, color: 'FFFFFF', bold: true, align: 'center',
239
+ fontFace: FONTS.heading,
240
+ });
241
+ if (props.subtitle) {
242
+ slide.addText(props.subtitle, {
243
+ x: 1.5, y: 3.5, w: 7, h: 0.7,
244
+ fontSize: 22, color: '94A3B8', align: 'center',
245
+ fontFace: FONTS.body,
246
+ });
247
+ }
248
+ if (props.date) {
249
+ slide.addText(props.date, {
250
+ x: 1, y: 4.6, w: 8, h: 0.3,
251
+ fontSize: 14, color: '94A3B8', align: 'center',
252
+ fontFace: FONTS.mono,
253
+ });
254
+ }
255
+ }
219
256
  function renderTableOfContentsV1(slide, props) {
220
257
  addKicker(slide, props.kicker);
221
258
  addTitle(slide, props.title);
@@ -258,6 +295,33 @@ function renderMetricV1(slide, props) {
258
295
  });
259
296
  }
260
297
  }
298
+ function renderDarkTechMetricV1(slide, props) {
299
+ slide.background = { color: '0A0F1C' };
300
+ slide.addShape('ellipse', {
301
+ x: 2.5, y: 0.5, w: 5, h: 4.5,
302
+ fill: { color: '06B6D4' },
303
+ });
304
+ if (props.label) {
305
+ slide.addText(props.label, {
306
+ x: 1, y: 1.6, w: 8, h: 0.4,
307
+ fontSize: 15, color: '06B6D4', align: 'center',
308
+ fontFace: FONTS.mono,
309
+ });
310
+ }
311
+ const metricText = props.unit ? `${props.value} ${props.unit}` : props.value;
312
+ slide.addText(metricText, {
313
+ x: 1, y: 2.2, w: 8, h: 1.5,
314
+ fontSize: 96, color: 'FFFFFF', bold: true, align: 'center',
315
+ fontFace: FONTS.heading,
316
+ });
317
+ if (props.description) {
318
+ slide.addText(props.description, {
319
+ x: 1.5, y: 4.0, w: 7, h: 0.8,
320
+ fontSize: 20, color: '94A3B8', align: 'center',
321
+ fontFace: FONTS.body,
322
+ });
323
+ }
324
+ }
261
325
  function renderMetricV2(slide, props) {
262
326
  if (props.kicker) {
263
327
  slide.addText(props.kicker, {
@@ -354,7 +418,7 @@ function renderChartV2(slide, props) {
354
418
  labels,
355
419
  values: dataset.data || [],
356
420
  }));
357
- const chartColors = datasets.map((d, i) => d.color || DEFAULT_CHART_COLORS[i % DEFAULT_CHART_COLORS.length]);
421
+ const chartColors = datasets.map((d, i) => d.color || CHART_COLORS[i % CHART_COLORS.length]);
358
422
  slide.addChart('bar', chartData, {
359
423
  x: 0.8, y: 2.3, w: 8.4, h: 3.2,
360
424
  chartColors,
@@ -504,6 +568,60 @@ function renderTimelineV1(slide, props) {
504
568
  });
505
569
  });
506
570
  }
571
+ function renderWarmBusinessTimelineV1(slide, props) {
572
+ addKicker(slide, props.kicker);
573
+ addTitle(slide, props.title);
574
+ const milestones = (props.milestones || []).slice(0, 4);
575
+ const count = milestones.length || 1;
576
+ const gap = 0.25;
577
+ const cardW = (8.4 - gap * (count - 1)) / count;
578
+ const startX = 0.8;
579
+ const y = 2.7;
580
+ const h = 3.2;
581
+ const warmAccent = 'D97706';
582
+ const warmText = '3E2723';
583
+ const warmSecondary = '6D4C41';
584
+ milestones.forEach((m, index) => {
585
+ const x = startX + index * (cardW + gap);
586
+ slide.addShape('rect', {
587
+ x, y, w: cardW, h,
588
+ fill: { color: 'FFF8F0' },
589
+ line: { color: 'F3E5D8', width: 1 },
590
+ rectRadius: 0.12,
591
+ });
592
+ if (index < count - 1) {
593
+ slide.addShape('rect', {
594
+ x: x + cardW, y: y + 0.35, w: gap, h: 0.04,
595
+ fill: { color: warmAccent },
596
+ });
597
+ slide.addShape('ellipse', {
598
+ x: x + cardW + gap - 0.06, y: y + 0.30, w: 0.12, h: 0.12,
599
+ fill: { color: warmAccent },
600
+ });
601
+ }
602
+ if (m.date) {
603
+ slide.addText(m.date, {
604
+ x: x + 0.16, y: y + 0.22, w: cardW - 0.32, h: 0.3,
605
+ fontSize: 12, color: warmAccent, align: 'left', valign: 'top',
606
+ fontFace: FONTS.mono,
607
+ });
608
+ }
609
+ if (m.title) {
610
+ slide.addText(m.title, {
611
+ x: x + 0.16, y: y + 0.6, w: cardW - 0.32, h: 0.45,
612
+ fontSize: 17, color: warmText, bold: true, align: 'left', valign: 'top',
613
+ fontFace: FONTS.heading,
614
+ });
615
+ }
616
+ if (m.description) {
617
+ slide.addText(m.description, {
618
+ x: x + 0.16, y: y + 1.1, w: cardW - 0.32, h: 1.6,
619
+ fontSize: 13, color: warmSecondary, align: 'left', valign: 'top',
620
+ fontFace: FONTS.body,
621
+ });
622
+ }
623
+ });
624
+ }
507
625
  function renderRoadmapV1(slide, props) {
508
626
  addKicker(slide, props.kicker);
509
627
  addTitle(slide, props.title);
@@ -579,6 +697,38 @@ function renderTestimonialV1(slide, props) {
579
697
  });
580
698
  }
581
699
  }
700
+ function renderWarmBusinessTestimonialV1(slide, props) {
701
+ const warmAccent = 'D97706';
702
+ const warmText = '3E2723';
703
+ const warmSecondary = '6D4C41';
704
+ const warmSurface = 'FFF8F0';
705
+ slide.addShape('rect', {
706
+ x: 0.8, y: 1.4, w: 8.4, h: 4.2,
707
+ fill: { color: warmSurface },
708
+ line: { color: 'F3E5D8', width: 1 },
709
+ rectRadius: 0.12,
710
+ });
711
+ slide.addText('“', {
712
+ x: 1.0, y: 1.5, w: 0.8, h: 0.8,
713
+ fontSize: 64, color: warmAccent, align: 'left', valign: 'top',
714
+ fontFace: FONTS.heading,
715
+ });
716
+ slide.addText(props.quote, {
717
+ x: 1.2, y: 2.0, w: 7.6, h: 2.0,
718
+ fontSize: 32, color: warmText, bold: true, align: 'center', valign: 'middle',
719
+ fontFace: FONTS.heading,
720
+ });
721
+ if (props.avatarUrl)
722
+ addImageMaybe(slide, props.avatarUrl, 1.4, 4.15, 0.7, 0.7);
723
+ const attribution = [props.author, props.role, props.company].filter(Boolean).join(' · ');
724
+ if (attribution) {
725
+ slide.addText(attribution, {
726
+ x: 2.3, y: 4.3, w: 6.5, h: 0.4,
727
+ fontSize: 15, color: warmSecondary, align: 'left', valign: 'middle',
728
+ fontFace: FONTS.mono,
729
+ });
730
+ }
731
+ }
582
732
  function renderTestimonialV2(slide, props) {
583
733
  // 卡片背景
584
734
  slide.addShape('rect', {
@@ -673,6 +823,38 @@ function renderFeatureV1(slide, props) {
673
823
  });
674
824
  });
675
825
  }
826
+ function renderDarkTechFeatureV1(slide, props) {
827
+ addKicker(slide, props.kicker);
828
+ addTitle(slide, props.title);
829
+ const features = (props.features || []).slice(0, 3);
830
+ const cardWidth = 2.6;
831
+ const gap = 0.3;
832
+ const startX = (10 - (features.length * cardWidth + (features.length - 1) * gap)) / 2;
833
+ features.forEach((feature, index) => {
834
+ const x = startX + index * (cardWidth + gap);
835
+ slide.addShape('rect', {
836
+ x, y: 2.8, w: cardWidth, h: 2.2,
837
+ fill: { color: '111827' },
838
+ line: { color: '06B6D4', width: 1 },
839
+ rectRadius: 0.1,
840
+ });
841
+ slide.addText(String(index + 1).padStart(2, '0'), {
842
+ x: x + 0.15, y: 2.95, w: cardWidth - 0.3, h: 0.35,
843
+ fontSize: 14, color: '06B6D4', align: 'left', valign: 'top',
844
+ fontFace: FONTS.mono,
845
+ });
846
+ slide.addText(feature.title || '', {
847
+ x: x + 0.15, y: 3.35, w: cardWidth - 0.3, h: 0.45,
848
+ fontSize: 20, color: 'FFFFFF', bold: true, align: 'left', valign: 'top',
849
+ fontFace: FONTS.heading,
850
+ });
851
+ slide.addText(feature.description || '', {
852
+ x: x + 0.15, y: 3.85, w: cardWidth - 0.3, h: 0.95,
853
+ fontSize: 14, color: '94A3B8', align: 'left', valign: 'top',
854
+ fontFace: FONTS.body,
855
+ });
856
+ });
857
+ }
676
858
  function renderTeamV1(slide, props) {
677
859
  addKicker(slide, props.kicker);
678
860
  addTitle(slide, props.title);
@@ -953,4 +1135,743 @@ function renderClosingV2(slide, props) {
953
1135
  y += 0.45;
954
1136
  });
955
1137
  }
1138
+ function renderTimelineV2(slide, props) {
1139
+ addKicker(slide, props.kicker);
1140
+ addTitle(slide, props.title ?? 'Timeline');
1141
+ const milestones = props.milestones ?? [];
1142
+ const maxItems = 5;
1143
+ let y = 2.6;
1144
+ milestones.slice(0, maxItems).forEach((milestone, index) => {
1145
+ const isLast = index === Math.min(milestones.length, maxItems) - 1;
1146
+ // marker dot
1147
+ slide.addShape('ellipse', {
1148
+ x: 0.9, y: y + 0.16, w: 0.16, h: 0.16,
1149
+ fill: { color: COLORS.accent },
1150
+ });
1151
+ if (!isLast) {
1152
+ slide.addShape('rect', {
1153
+ x: 0.97, y: y + 0.42, w: 0.02, h: 1.0,
1154
+ fill: { color: COLORS.border },
1155
+ });
1156
+ }
1157
+ const dateText = milestone.date ?? '';
1158
+ if (dateText) {
1159
+ slide.addText(dateText, {
1160
+ x: 1.3, y, w: 2.0, h: 0.35,
1161
+ fontSize: 14, color: COLORS.accent, align: 'left', valign: 'top',
1162
+ fontFace: FONTS.mono,
1163
+ });
1164
+ }
1165
+ if (milestone.title) {
1166
+ slide.addText(milestone.title, {
1167
+ x: 1.3, y: y + 0.35, w: 7.8, h: 0.4,
1168
+ fontSize: 20, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1169
+ fontFace: FONTS.heading,
1170
+ });
1171
+ }
1172
+ if (milestone.description) {
1173
+ slide.addText(milestone.description, {
1174
+ x: 1.3, y: y + 0.75, w: 7.8, h: 0.55,
1175
+ fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1176
+ fontFace: FONTS.body,
1177
+ });
1178
+ }
1179
+ y += 1.35;
1180
+ });
1181
+ }
1182
+ function renderRoadmapV2(slide, props) {
1183
+ addKicker(slide, props.kicker);
1184
+ addTitle(slide, props.title ?? 'Roadmap');
1185
+ const phases = props.phases ?? [];
1186
+ const maxPhases = 4;
1187
+ const cardW = 2.1;
1188
+ const startX = 0.7;
1189
+ const y = 2.6;
1190
+ const h = 3.4;
1191
+ phases.slice(0, maxPhases).forEach((phase, index) => {
1192
+ const x = startX + index * (cardW + 0.25);
1193
+ slide.addShape('rect', {
1194
+ x, y, w: cardW, h,
1195
+ fill: { color: COLORS.surfaceElevated },
1196
+ line: { color: COLORS.border, width: 1 },
1197
+ rectRadius: 0.08,
1198
+ });
1199
+ slide.addText(phase.phase ?? `Phase ${index + 1}`, {
1200
+ x: x + 0.12, y: y + 0.2, w: cardW - 0.24, h: 0.4,
1201
+ fontSize: 18, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1202
+ fontFace: FONTS.heading,
1203
+ });
1204
+ const goals = (phase.goals ?? []).slice(0, 5);
1205
+ let goalY = y + 0.7;
1206
+ goals.forEach((goal) => {
1207
+ slide.addText(`• ${goal}`, {
1208
+ x: x + 0.16, y: goalY, w: cardW - 0.32, h: 0.45,
1209
+ fontSize: 13, color: COLORS.secondary, align: 'left', valign: 'top',
1210
+ fontFace: FONTS.body,
1211
+ });
1212
+ goalY += 0.45;
1213
+ });
1214
+ });
1215
+ }
1216
+ function renderPricingV2(slide, props) {
1217
+ addKicker(slide, props.kicker);
1218
+ addTitle(slide, props.title ?? 'Pricing');
1219
+ const plans = props.plans ?? [];
1220
+ const maxPlans = 3;
1221
+ const cardW = 2.6;
1222
+ const startX = 0.9;
1223
+ const y = 2.6;
1224
+ const h = 3.8;
1225
+ plans.slice(0, maxPlans).forEach((plan, index) => {
1226
+ const x = startX + index * (cardW + 0.25);
1227
+ const isHighlighted = plan.highlighted;
1228
+ slide.addShape('rect', {
1229
+ x, y, w: cardW, h,
1230
+ fill: { color: isHighlighted ? COLORS.accent : COLORS.surfaceElevated },
1231
+ line: { color: isHighlighted ? COLORS.accent : COLORS.border, width: 1 },
1232
+ rectRadius: 0.08,
1233
+ });
1234
+ slide.addText(plan.name ?? `Plan ${index + 1}`, {
1235
+ x: x + 0.15, y: y + 0.25, w: cardW - 0.3, h: 0.4,
1236
+ fontSize: 20, color: isHighlighted ? 'FFFFFF' : COLORS.primary, bold: true, align: 'center', valign: 'top',
1237
+ fontFace: FONTS.heading,
1238
+ });
1239
+ slide.addText(`${plan.price ?? ''} ${plan.period ?? ''}`, {
1240
+ x: x + 0.15, y: y + 0.75, w: cardW - 0.3, h: 0.5,
1241
+ fontSize: 24, color: isHighlighted ? 'FFFFFF' : COLORS.accent, bold: true, align: 'center', valign: 'top',
1242
+ fontFace: FONTS.heading,
1243
+ });
1244
+ const features = (plan.features ?? []).slice(0, 5);
1245
+ let featY = y + 1.4;
1246
+ features.forEach((feature) => {
1247
+ slide.addText(`• ${feature}`, {
1248
+ x: x + 0.2, y: featY, w: cardW - 0.4, h: 0.4,
1249
+ fontSize: 13, color: isHighlighted ? 'FFFFFF' : COLORS.secondary, align: 'left', valign: 'top',
1250
+ fontFace: FONTS.body,
1251
+ });
1252
+ featY += 0.4;
1253
+ });
1254
+ });
1255
+ }
1256
+ function renderFeatureV2(slide, props) {
1257
+ addKicker(slide, props.kicker);
1258
+ addTitle(slide, props.title ?? 'Features');
1259
+ const features = props.features ?? [];
1260
+ const maxFeatures = 3;
1261
+ const cardW = 2.8;
1262
+ const startX = 0.7;
1263
+ const y = 2.6;
1264
+ const h = 3.4;
1265
+ features.slice(0, maxFeatures).forEach((feature, index) => {
1266
+ const x = startX + index * (cardW + 0.25);
1267
+ slide.addShape('rect', {
1268
+ x, y, w: cardW, h,
1269
+ fill: { color: COLORS.surfaceElevated },
1270
+ line: { color: COLORS.border, width: 1 },
1271
+ rectRadius: 0.08,
1272
+ });
1273
+ slide.addText(feature.icon ?? '◆', {
1274
+ x: x + 0.2, y: y + 0.25, w: cardW - 0.4, h: 0.5,
1275
+ fontSize: 28, color: COLORS.accent, align: 'left', valign: 'top',
1276
+ fontFace: FONTS.body,
1277
+ });
1278
+ slide.addText(feature.title ?? `Feature ${index + 1}`, {
1279
+ x: x + 0.2, y: y + 0.85, w: cardW - 0.4, h: 0.4,
1280
+ fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1281
+ fontFace: FONTS.heading,
1282
+ });
1283
+ slide.addText(feature.description ?? '', {
1284
+ x: x + 0.2, y: y + 1.35, w: cardW - 0.4, h: 1.8,
1285
+ fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
1286
+ fontFace: FONTS.body,
1287
+ });
1288
+ });
1289
+ }
1290
+ function renderTeamV2(slide, props) {
1291
+ addKicker(slide, props.kicker);
1292
+ addTitle(slide, props.title ?? 'Team');
1293
+ const members = props.members ?? [];
1294
+ const maxMembers = 4;
1295
+ const cardW = 2.2;
1296
+ const startX = 0.6;
1297
+ const y = 2.5;
1298
+ const h = 3.8;
1299
+ members.slice(0, maxMembers).forEach((member, index) => {
1300
+ const x = startX + index * (cardW + 0.25);
1301
+ slide.addShape('rect', {
1302
+ x, y, w: cardW, h,
1303
+ fill: { color: COLORS.surfaceElevated },
1304
+ line: { color: COLORS.border, width: 1 },
1305
+ rectRadius: 0.08,
1306
+ });
1307
+ slide.addShape('ellipse', {
1308
+ x: x + cardW / 2 - 0.35, y: y + 0.25, w: 0.7, h: 0.7,
1309
+ fill: { color: COLORS.border },
1310
+ });
1311
+ slide.addText(member.name ?? `Member ${index + 1}`, {
1312
+ x: x + 0.15, y: y + 1.05, w: cardW - 0.3, h: 0.35,
1313
+ fontSize: 17, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1314
+ fontFace: FONTS.heading,
1315
+ });
1316
+ slide.addText(member.role ?? '', {
1317
+ x: x + 0.15, y: y + 1.45, w: cardW - 0.3, h: 0.3,
1318
+ fontSize: 13, color: COLORS.accent, align: 'center', valign: 'top',
1319
+ fontFace: FONTS.body,
1320
+ });
1321
+ slide.addText(member.bio ?? '', {
1322
+ x: x + 0.15, y: y + 1.85, w: cardW - 0.3, h: 1.7,
1323
+ fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
1324
+ fontFace: FONTS.body,
1325
+ });
1326
+ });
1327
+ }
1328
+ function renderMetricV3(slide, props) {
1329
+ addKicker(slide, props.kicker);
1330
+ addTitle(slide, props.title ?? 'Metrics');
1331
+ const metrics = props.metrics ?? [];
1332
+ const maxMetrics = 2;
1333
+ const cardW = 4.0;
1334
+ const startX = 1.0;
1335
+ const y = 2.8;
1336
+ const h = 3.0;
1337
+ metrics.slice(0, maxMetrics).forEach((metric, index) => {
1338
+ const x = startX + index * (cardW + 0.5);
1339
+ slide.addShape('rect', {
1340
+ x, y, w: cardW, h,
1341
+ fill: { color: COLORS.surfaceElevated },
1342
+ line: { color: COLORS.border, width: 1 },
1343
+ rectRadius: 0.08,
1344
+ });
1345
+ slide.addText(metric.label ?? `Metric ${index + 1}`, {
1346
+ x: x + 0.25, y: y + 0.3, w: cardW - 0.5, h: 0.4,
1347
+ fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1348
+ fontFace: FONTS.body,
1349
+ });
1350
+ slide.addText(`${metric.value ?? ''}${metric.unit ? ` ${metric.unit}` : ''}`, {
1351
+ x: x + 0.25, y: y + 0.85, w: cardW - 0.5, h: 0.8,
1352
+ fontSize: 48, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1353
+ fontFace: FONTS.heading,
1354
+ });
1355
+ if (metric.change) {
1356
+ slide.addText(metric.change, {
1357
+ x: x + 0.25, y: y + 1.85, w: cardW - 0.5, h: 0.4,
1358
+ fontSize: 16, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1359
+ fontFace: FONTS.body,
1360
+ });
1361
+ }
1362
+ });
1363
+ }
1364
+ function renderCoverV2(slide, props) {
1365
+ if (props.image) {
1366
+ addImageMaybe(slide, props.image, 5, 0, 5, 5.625);
1367
+ }
1368
+ else {
1369
+ slide.addShape('rect', {
1370
+ x: 5, y: 0.8, w: 4.5, h: 4.0,
1371
+ fill: { color: COLORS.accent },
1372
+ rectRadius: 0.2,
1373
+ });
1374
+ }
1375
+ if (props.kicker) {
1376
+ slide.addText(props.kicker, {
1377
+ x: 0.8, y: 1.6, w: 4.0, h: 0.4,
1378
+ fontSize: 14, color: COLORS.accent, align: 'left',
1379
+ fontFace: FONTS.mono,
1380
+ });
1381
+ }
1382
+ slide.addText(props.title, {
1383
+ x: 0.8, y: 2.1, w: 4.2, h: 1.2,
1384
+ fontSize: 44, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1385
+ fontFace: FONTS.heading,
1386
+ });
1387
+ if (props.subtitle) {
1388
+ slide.addText(props.subtitle, {
1389
+ x: 0.8, y: 3.4, w: 4.2, h: 0.8,
1390
+ fontSize: 18, color: COLORS.secondary, align: 'left', valign: 'top',
1391
+ fontFace: FONTS.body,
1392
+ });
1393
+ }
1394
+ if (props.date) {
1395
+ slide.addText(props.date, {
1396
+ x: 0.8, y: 4.5, w: 4.0, h: 0.3,
1397
+ fontSize: 13, color: COLORS.secondary, align: 'left',
1398
+ fontFace: FONTS.mono,
1399
+ });
1400
+ }
1401
+ }
1402
+ function renderContentV4(slide, props) {
1403
+ addKicker(slide, props.kicker);
1404
+ addTitle(slide, props.title);
1405
+ const cards = (props.cards || []).slice(0, 3);
1406
+ if (cards.length === 0)
1407
+ return;
1408
+ const cardW = 2.6;
1409
+ const gap = 0.3;
1410
+ const startX = (10 - (cards.length * cardW + (cards.length - 1) * gap)) / 2;
1411
+ cards.forEach((card, index) => {
1412
+ const x = startX + index * (cardW + gap);
1413
+ slide.addShape('rect', {
1414
+ x, y: 2.8, w: cardW, h: 2.4,
1415
+ fill: { color: COLORS.surfaceElevated },
1416
+ line: { color: COLORS.border, width: 1 },
1417
+ });
1418
+ slide.addText(card.title || '', {
1419
+ x: x + 0.15, y: 3.0, w: cardW - 0.3, h: 0.5,
1420
+ fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1421
+ fontFace: FONTS.heading,
1422
+ });
1423
+ slide.addText(card.description || '', {
1424
+ x: x + 0.15, y: 3.55, w: cardW - 0.3, h: 1.5,
1425
+ fontSize: 13, color: COLORS.secondary, align: 'left', valign: 'top',
1426
+ fontFace: FONTS.body,
1427
+ });
1428
+ });
1429
+ }
1430
+ function renderStatsV2(slide, props) {
1431
+ addKicker(slide, props.kicker);
1432
+ addTitle(slide, props.title);
1433
+ const stats = (props.stats || []).slice(0, 4);
1434
+ if (stats.length === 0)
1435
+ return;
1436
+ const itemW = 8.4 / stats.length;
1437
+ stats.forEach((stat, index) => {
1438
+ const x = 0.8 + index * itemW;
1439
+ slide.addShape('rect', {
1440
+ x: x + 0.1, y: 2.8, w: itemW - 0.2, h: 2.4,
1441
+ fill: { color: COLORS.surfaceElevated },
1442
+ line: { color: COLORS.border, width: 1 },
1443
+ });
1444
+ const valueText = [stat.value, stat.unit].filter(Boolean).join(' ') || '-';
1445
+ slide.addText(valueText, {
1446
+ x: x + 0.2, y: 3.0, w: itemW - 0.4, h: 0.8,
1447
+ fontSize: 36, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1448
+ fontFace: FONTS.heading,
1449
+ });
1450
+ slide.addText(stat.label || '', {
1451
+ x: x + 0.2, y: 3.9, w: itemW - 0.4, h: 0.4,
1452
+ fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'top',
1453
+ fontFace: FONTS.body,
1454
+ });
1455
+ if (stat.change) {
1456
+ slide.addText(stat.change, {
1457
+ x: x + 0.2, y: 4.35, w: itemW - 0.4, h: 0.35,
1458
+ fontSize: 13, color: COLORS.accent, align: 'center',
1459
+ fontFace: FONTS.mono,
1460
+ });
1461
+ }
1462
+ });
1463
+ }
1464
+ function renderImageV2(slide, props) {
1465
+ if (props.image) {
1466
+ addImageMaybe(slide, props.image, 4.2, 0, 5.8, 5.625);
1467
+ }
1468
+ if (props.caption) {
1469
+ slide.addText(props.caption, {
1470
+ x: 4.2, y: 5.0, w: 5.8, h: 0.4,
1471
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'bottom',
1472
+ fontFace: FONTS.mono,
1473
+ });
1474
+ }
1475
+ if (props.kicker) {
1476
+ slide.addText(props.kicker, {
1477
+ x: 0.8, y: 1.4, w: 3.0, h: 0.4,
1478
+ fontSize: 14, color: COLORS.accent, align: 'left',
1479
+ fontFace: FONTS.mono,
1480
+ });
1481
+ }
1482
+ slide.addText(props.title, {
1483
+ x: 0.8, y: 1.9, w: 3.2, h: 1.2,
1484
+ fontSize: 38, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1485
+ fontFace: FONTS.heading,
1486
+ });
1487
+ if (props.body) {
1488
+ slide.addText(props.body, {
1489
+ x: 0.8, y: 3.2, w: 3.2, h: 1.8,
1490
+ fontSize: 15, color: COLORS.secondary, align: 'left', valign: 'top',
1491
+ fontFace: FONTS.body,
1492
+ });
1493
+ }
1494
+ }
1495
+ function renderQuoteV3(slide, props) {
1496
+ slide.addText('"', {
1497
+ x: 0.8, y: 1.0, w: 1.5, h: 1.5,
1498
+ fontSize: 120, color: COLORS.accent, align: 'left', valign: 'top',
1499
+ fontFace: FONTS.heading,
1500
+ });
1501
+ slide.addText(props.quote, {
1502
+ x: 2.0, y: 1.6, w: 7.2, h: 2.6,
1503
+ fontSize: 28, color: COLORS.primary, align: 'left', valign: 'top',
1504
+ fontFace: FONTS.heading,
1505
+ });
1506
+ const meta = [props.author, props.role].filter(Boolean).join(' · ');
1507
+ if (meta) {
1508
+ slide.addText(meta, {
1509
+ x: 2.0, y: 4.4, w: 7.2, h: 0.4,
1510
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
1511
+ fontFace: FONTS.body,
1512
+ });
1513
+ }
1514
+ if (props.source) {
1515
+ slide.addText(props.source, {
1516
+ x: 2.0, y: 4.85, w: 7.2, h: 0.3,
1517
+ fontSize: 12, color: COLORS.secondary, align: 'left',
1518
+ fontFace: FONTS.mono,
1519
+ });
1520
+ }
1521
+ }
1522
+ function renderProcessV3(slide, props) {
1523
+ addKicker(slide, props.kicker);
1524
+ addTitle(slide, props.title);
1525
+ const steps = (props.steps || []).slice(0, 4);
1526
+ if (steps.length === 0)
1527
+ return;
1528
+ const stepH = 0.85;
1529
+ const gap = 0.18;
1530
+ const startY = 2.6;
1531
+ steps.forEach((step, index) => {
1532
+ const y = startY + index * (stepH + gap);
1533
+ slide.addShape('rect', {
1534
+ x: 0.8, y, w: 8.4, h: stepH,
1535
+ fill: { color: COLORS.surfaceElevated },
1536
+ line: { color: COLORS.border, width: 1 },
1537
+ });
1538
+ slide.addShape('rect', {
1539
+ x: 0.8, y, w: 0.6, h: stepH,
1540
+ fill: { color: COLORS.accent },
1541
+ });
1542
+ slide.addText(String(index + 1).padStart(2, '0'), {
1543
+ x: 0.8, y, w: 0.6, h: stepH,
1544
+ fontSize: 14, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle',
1545
+ fontFace: FONTS.mono,
1546
+ });
1547
+ slide.addText(step.title || '', {
1548
+ x: 1.6, y: y + 0.12, w: 7.4, h: 0.3,
1549
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
1550
+ fontFace: FONTS.heading,
1551
+ });
1552
+ slide.addText(step.description || '', {
1553
+ x: 1.6, y: y + 0.42, w: 7.4, h: 0.35,
1554
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1555
+ fontFace: FONTS.body,
1556
+ });
1557
+ });
1558
+ }
1559
+ function renderFeatureV3(slide, props) {
1560
+ if (props.image) {
1561
+ addImageMaybe(slide, props.image, 0, 0, 4.5, 5.625);
1562
+ }
1563
+ if (props.kicker) {
1564
+ slide.addText(props.kicker, {
1565
+ x: 5.0, y: 0.8, w: 4.2, h: 0.4,
1566
+ fontSize: 14, color: COLORS.accent, align: 'left',
1567
+ fontFace: FONTS.mono,
1568
+ });
1569
+ }
1570
+ slide.addText(props.title, {
1571
+ x: 5.0, y: 1.3, w: 4.2, h: 0.9,
1572
+ fontSize: 36, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1573
+ fontFace: FONTS.heading,
1574
+ });
1575
+ const features = (props.features || []).slice(0, 3);
1576
+ features.forEach((feature, index) => {
1577
+ const y = 2.4 + index * 1.0;
1578
+ slide.addShape('ellipse', {
1579
+ x: 5.0, y: y + 0.1, w: 0.2, h: 0.2,
1580
+ fill: { color: COLORS.accent },
1581
+ });
1582
+ slide.addText(feature.title || '', {
1583
+ x: 5.4, y, w: 3.8, h: 0.35,
1584
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
1585
+ fontFace: FONTS.heading,
1586
+ });
1587
+ slide.addText(feature.description || '', {
1588
+ x: 5.4, y: y + 0.35, w: 3.8, h: 0.55,
1589
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1590
+ fontFace: FONTS.body,
1591
+ });
1592
+ });
1593
+ }
1594
+ function renderGalleryV3(slide, props) {
1595
+ addKicker(slide, props.kicker);
1596
+ addTitle(slide, props.title ?? 'GalleryV3');
1597
+ const images = (props.images ?? []).slice(0, 3);
1598
+ const gap = 0.35;
1599
+ const cardW = (8.4 - gap * 2) / 3;
1600
+ const startX = 0.8;
1601
+ const y = 2.6;
1602
+ const imgH = 2.8;
1603
+ images.forEach((image, index) => {
1604
+ const x = startX + index * (cardW + gap);
1605
+ slide.addShape('rect', {
1606
+ x,
1607
+ y,
1608
+ w: cardW,
1609
+ h: imgH,
1610
+ fill: { color: COLORS.surfaceElevated },
1611
+ line: { color: COLORS.border, width: 1 },
1612
+ rectRadius: 0.08,
1613
+ });
1614
+ if (image.url) {
1615
+ try {
1616
+ slide.addImage({ path: image.url, x: x + 0.1, y: y + 0.1, w: cardW - 0.2, h: imgH - 0.6 });
1617
+ }
1618
+ catch {
1619
+ slide.addShape('rect', {
1620
+ x: x + 0.1, y: y + 0.1, w: cardW - 0.2, h: imgH - 0.6,
1621
+ fill: { color: COLORS.accent },
1622
+ });
1623
+ }
1624
+ }
1625
+ slide.addText(image.caption || '', {
1626
+ x,
1627
+ y: y + imgH - 0.45,
1628
+ w: cardW,
1629
+ h: 0.4,
1630
+ fontSize: 12,
1631
+ color: COLORS.secondary,
1632
+ align: 'center',
1633
+ valign: 'middle',
1634
+ fontFace: FONTS.body,
1635
+ });
1636
+ });
1637
+ }
1638
+ function renderTestimonialV3(slide, props) {
1639
+ const leftX = 0.8;
1640
+ const leftW = 5.0;
1641
+ const rightX = 6.2;
1642
+ const rightW = 3.0;
1643
+ slide.addText('"', {
1644
+ x: leftX, y: 1.2, w: 1.0, h: 1.0,
1645
+ fontSize: 120, color: COLORS.accent, align: 'left', valign: 'top',
1646
+ fontFace: FONTS.heading,
1647
+ });
1648
+ slide.addText(props.quote ?? '', {
1649
+ x: leftX, y: 2.2, w: leftW, h: 3.2,
1650
+ fontSize: 28, color: COLORS.primary, align: 'left', valign: 'top',
1651
+ fontFace: FONTS.heading,
1652
+ });
1653
+ slide.addShape('rect', {
1654
+ x: rightX, y: 1.4, w: rightW, h: 4.4,
1655
+ fill: { color: COLORS.surfaceElevated },
1656
+ line: { color: COLORS.border, width: 1 },
1657
+ rectRadius: 0.1,
1658
+ });
1659
+ if (props.avatar) {
1660
+ try {
1661
+ slide.addImage({ path: props.avatar, x: rightX + 0.8, y: 1.8, w: 1.4, h: 1.4 });
1662
+ }
1663
+ catch {
1664
+ slide.addShape('rect', {
1665
+ x: rightX + 0.8, y: 1.8, w: 1.4, h: 1.4,
1666
+ fill: { color: COLORS.accent },
1667
+ });
1668
+ }
1669
+ }
1670
+ slide.addText(props.author ?? '', {
1671
+ x: rightX, y: 3.5, w: rightW, h: 0.35,
1672
+ fontSize: 18, color: COLORS.primary, bold: true, align: 'center',
1673
+ fontFace: FONTS.heading,
1674
+ });
1675
+ slide.addText(props.role ?? '', {
1676
+ x: rightX, y: 3.85, w: rightW, h: 0.3,
1677
+ fontSize: 14, color: COLORS.secondary, align: 'center',
1678
+ fontFace: FONTS.body,
1679
+ });
1680
+ slide.addText(props.company ?? '', {
1681
+ x: rightX, y: 4.15, w: rightW, h: 0.3,
1682
+ fontSize: 13, color: COLORS.secondary, align: 'center',
1683
+ fontFace: FONTS.body,
1684
+ });
1685
+ if (props.metric) {
1686
+ slide.addText(props.metric, {
1687
+ x: rightX, y: 4.75, w: rightW, h: 0.45,
1688
+ fontSize: 28, color: COLORS.accent, bold: true, align: 'center',
1689
+ fontFace: FONTS.heading,
1690
+ });
1691
+ slide.addText(props.metricLabel ?? '', {
1692
+ x: rightX, y: 5.2, w: rightW, h: 0.3,
1693
+ fontSize: 12, color: COLORS.secondary, align: 'center',
1694
+ fontFace: FONTS.body,
1695
+ });
1696
+ }
1697
+ }
1698
+ function renderComparisonV3(slide, props) {
1699
+ addKicker(slide, props.kicker);
1700
+ addTitle(slide, props.title ?? 'ComparisonV3');
1701
+ const rows = (props.rows ?? []).slice(0, 5);
1702
+ const startY = 2.6;
1703
+ const rowH = 0.8;
1704
+ const col1W = 3.2;
1705
+ const col2W = 2.8;
1706
+ const col3W = 2.8;
1707
+ const gap = 0.1;
1708
+ const startX = 0.8;
1709
+ slide.addShape('rect', {
1710
+ x: startX, y: startY, w: col1W, h: 0.5,
1711
+ fill: { color: COLORS.surfaceElevated },
1712
+ line: { color: COLORS.border, width: 1 },
1713
+ });
1714
+ slide.addShape('rect', {
1715
+ x: startX + col1W + gap, y: startY, w: col2W, h: 0.5,
1716
+ fill: { color: COLORS.accent },
1717
+ line: { color: COLORS.accent, width: 1 },
1718
+ });
1719
+ slide.addShape('rect', {
1720
+ x: startX + col1W + gap + col2W + gap, y: startY, w: col3W, h: 0.5,
1721
+ fill: { color: COLORS.surfaceElevated },
1722
+ line: { color: COLORS.border, width: 1 },
1723
+ });
1724
+ slide.addText('维度', {
1725
+ x: startX, y: startY, w: col1W, h: 0.5,
1726
+ fontSize: 12, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle',
1727
+ fontFace: FONTS.body,
1728
+ });
1729
+ slide.addText(props.leftTitle ?? '方案 A', {
1730
+ x: startX + col1W + gap, y: startY, w: col2W, h: 0.5,
1731
+ fontSize: 12, color: '#ffffff', bold: true, align: 'center', valign: 'middle',
1732
+ fontFace: FONTS.heading,
1733
+ });
1734
+ slide.addText(props.rightTitle ?? '方案 B', {
1735
+ x: startX + col1W + gap + col2W + gap, y: startY, w: col3W, h: 0.5,
1736
+ fontSize: 12, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1737
+ fontFace: FONTS.heading,
1738
+ });
1739
+ rows.forEach((row, index) => {
1740
+ const y = startY + 0.6 + index * rowH;
1741
+ const bg = index % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
1742
+ slide.addShape('rect', {
1743
+ x: startX, y, w: col1W, h: rowH,
1744
+ fill: { color: bg },
1745
+ line: { color: COLORS.border, width: 1 },
1746
+ });
1747
+ slide.addShape('rect', {
1748
+ x: startX + col1W + gap, y, w: col2W, h: rowH,
1749
+ fill: { color: bg },
1750
+ line: { color: COLORS.border, width: 1 },
1751
+ });
1752
+ slide.addShape('rect', {
1753
+ x: startX + col1W + gap + col2W + gap, y, w: col3W, h: rowH,
1754
+ fill: { color: bg },
1755
+ line: { color: COLORS.border, width: 1 },
1756
+ });
1757
+ slide.addText(row.feature ?? '', {
1758
+ x: startX + 0.15, y, w: col1W - 0.3, h: rowH,
1759
+ fontSize: 13, color: COLORS.primary, align: 'left', valign: 'middle',
1760
+ fontFace: FONTS.body,
1761
+ });
1762
+ slide.addText(row.left ?? '', {
1763
+ x: startX + col1W + gap + 0.1, y, w: col2W - 0.2, h: rowH,
1764
+ fontSize: 13, color: COLORS.primary, align: 'center', valign: 'middle',
1765
+ fontFace: FONTS.body,
1766
+ });
1767
+ slide.addText(row.right ?? '', {
1768
+ x: startX + col1W + gap + col2W + gap + 0.1, y, w: col3W - 0.2, h: rowH,
1769
+ fontSize: 13, color: COLORS.primary, align: 'center', valign: 'middle',
1770
+ fontFace: FONTS.body,
1771
+ });
1772
+ });
1773
+ }
1774
+ function renderTimelineV3(slide, props) {
1775
+ addKicker(slide, props.kicker);
1776
+ addTitle(slide, props.title ?? 'TimelineV3');
1777
+ const events = (props.events ?? []).slice(0, 4);
1778
+ const startY = 2.6;
1779
+ const rowH = 1.2;
1780
+ const dotX = 1.2;
1781
+ const cardX = 1.8;
1782
+ const cardW = 7.4;
1783
+ events.forEach((event, index) => {
1784
+ const y = startY + index * rowH;
1785
+ slide.addShape('ellipse', {
1786
+ x: dotX - 0.12, y: y + 0.08, w: 0.24, h: 0.24,
1787
+ fill: { color: COLORS.accent },
1788
+ line: { color: COLORS.accent, width: 1 },
1789
+ });
1790
+ if (index < events.length - 1) {
1791
+ slide.addShape('line', {
1792
+ x1: dotX, y1: y + 0.36, x2: dotX, y2: y + rowH - 0.08,
1793
+ line: { color: COLORS.border, width: 2 },
1794
+ });
1795
+ }
1796
+ slide.addShape('rect', {
1797
+ x: cardX, y, w: cardW, h: rowH - 0.15,
1798
+ fill: { color: index % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated },
1799
+ line: { color: COLORS.border, width: 1 },
1800
+ rectRadius: 0.08,
1801
+ });
1802
+ slide.addText(event.date ?? '', {
1803
+ x: cardX + 0.2, y: y + 0.15, w: 2.0, h: 0.25,
1804
+ fontSize: 12, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1805
+ fontFace: FONTS.body,
1806
+ });
1807
+ slide.addText(event.title ?? '', {
1808
+ x: cardX + 0.2, y: y + 0.42, w: cardW - 0.4, h: 0.3,
1809
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1810
+ fontFace: FONTS.heading,
1811
+ });
1812
+ slide.addText(event.description ?? '', {
1813
+ x: cardX + 0.2, y: y + 0.72, w: cardW - 0.4, h: 0.35,
1814
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1815
+ fontFace: FONTS.body,
1816
+ });
1817
+ });
1818
+ }
1819
+ // ---- PPTX renderer registration -------------------------------------------
1820
+ // 按具体 layoutId 注册通用渲染器,保留同一角色下不同变体的差异。
1821
+ registerPptxLayoutRenderer('cover_v1', renderCoverV1);
1822
+ registerPptxLayoutRenderer('table_of_contents_v1', renderTableOfContentsV1);
1823
+ registerPptxLayoutRenderer('metric_v1', renderMetricV1);
1824
+ registerPptxLayoutRenderer('metric_v2', renderMetricV2);
1825
+ registerPptxLayoutRenderer('metric_v3', renderMetricV3);
1826
+ registerPptxLayoutRenderer('stats_v1', renderStatsV1);
1827
+ registerPptxLayoutRenderer('chart_v1', renderChartV1);
1828
+ registerPptxLayoutRenderer('chart_v2', renderChartV2);
1829
+ registerPptxLayoutRenderer('content_v1', renderContentV1);
1830
+ registerPptxLayoutRenderer('content_v2', renderContentV2);
1831
+ registerPptxLayoutRenderer('content_v3', renderContentV3);
1832
+ registerPptxLayoutRenderer('split_v1', renderSplitV1);
1833
+ registerPptxLayoutRenderer('comparison_v1', renderComparisonV1);
1834
+ registerPptxLayoutRenderer('comparison_v2', renderComparisonV2);
1835
+ registerPptxLayoutRenderer('process_v1', renderProcessV1);
1836
+ registerPptxLayoutRenderer('process_v2', renderProcessV2);
1837
+ registerPptxLayoutRenderer('timeline_v1', renderTimelineV1);
1838
+ registerPptxLayoutRenderer('timeline_v2', renderTimelineV2);
1839
+ registerPptxLayoutRenderer('roadmap_v1', renderRoadmapV1);
1840
+ registerPptxLayoutRenderer('roadmap_v2', renderRoadmapV2);
1841
+ registerPptxLayoutRenderer('quote_v1', renderQuoteV1);
1842
+ registerPptxLayoutRenderer('quote_v2', renderQuoteV2);
1843
+ registerPptxLayoutRenderer('testimonial_v1', renderTestimonialV1);
1844
+ registerPptxLayoutRenderer('testimonial_v2', renderTestimonialV2);
1845
+ registerPptxLayoutRenderer('faq_v1', renderFaqV1);
1846
+ registerPptxLayoutRenderer('feature_v1', renderFeatureV1);
1847
+ registerPptxLayoutRenderer('feature_v2', renderFeatureV2);
1848
+ registerPptxLayoutRenderer('team_v1', renderTeamV1);
1849
+ registerPptxLayoutRenderer('team_v2', renderTeamV2);
1850
+ registerPptxLayoutRenderer('partners_v1', renderPartnersV1);
1851
+ registerPptxLayoutRenderer('pricing_v1', renderPricingV1);
1852
+ registerPptxLayoutRenderer('pricing_v2', renderPricingV2);
1853
+ registerPptxLayoutRenderer('gallery_v1', renderGalleryV1);
1854
+ registerPptxLayoutRenderer('gallery_v2', renderGalleryV2);
1855
+ registerPptxLayoutRenderer('image_v1', renderImageV1);
1856
+ registerPptxLayoutRenderer('swot_v1', renderSwotV1);
1857
+ registerPptxLayoutRenderer('pest_v1', renderPestV1);
1858
+ registerPptxLayoutRenderer('closing_v1', renderClosingV1);
1859
+ registerPptxLayoutRenderer('closing_v2', renderClosingV2);
1860
+ registerPptxLayoutRenderer('timeline_v3', renderTimelineV3);
1861
+ registerPptxLayoutRenderer('comparison_v3', renderComparisonV3);
1862
+ registerPptxLayoutRenderer('testimonial_v3', renderTestimonialV3);
1863
+ registerPptxLayoutRenderer('gallery_v3', renderGalleryV3);
1864
+ registerPptxLayoutRenderer('feature_v3', renderFeatureV3);
1865
+ registerPptxLayoutRenderer('process_v3', renderProcessV3);
1866
+ registerPptxLayoutRenderer('quote_v3', renderQuoteV3);
1867
+ registerPptxLayoutRenderer('image_v2', renderImageV2);
1868
+ registerPptxLayoutRenderer('stats_v2', renderStatsV2);
1869
+ registerPptxLayoutRenderer('content_v4', renderContentV4);
1870
+ registerPptxLayoutRenderer('cover_v2', renderCoverV2);
1871
+ // 主题专属变体(阶段 3)
1872
+ registerPptxLayoutRenderer('dark_tech_cover_v1', renderDarkTechCoverV1);
1873
+ registerPptxLayoutRenderer('dark_tech_feature_v1', renderDarkTechFeatureV1);
1874
+ registerPptxLayoutRenderer('dark_tech_metric_v1', renderDarkTechMetricV1);
1875
+ registerPptxLayoutRenderer('warm_business_timeline_v1', renderWarmBusinessTimelineV1);
1876
+ registerPptxLayoutRenderer('warm_business_testimonial_v1', renderWarmBusinessTestimonialV1);
956
1877
  //# sourceMappingURL=export-pptx.js.map