@deckio/deck-engine 1.7.5 ā 1.7.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.
- package/components/BottomBar.jsx +9 -9
- package/components/BottomBar.module.css +17 -17
- package/components/Navigation.jsx +155 -106
- package/components/Navigation.module.css +154 -145
- package/components/Slide.jsx +15 -15
- package/components/exportDeckPdf.js +134 -0
- package/context/SlideContext.jsx +171 -171
- package/index.js +5 -5
- package/instructions/AGENTS.md +26 -26
- package/instructions/deck-config.instructions.md +34 -34
- package/instructions/deck-project.instructions.md +34 -34
- package/instructions/slide-css.instructions.md +91 -91
- package/instructions/slide-jsx.instructions.md +34 -34
- package/package.json +49 -45
- package/scripts/capture-screen.mjs +110 -110
- package/scripts/export-pdf.mjs +287 -287
- package/scripts/generate-image.mjs +110 -110
- package/scripts/init-project.mjs +214 -188
- package/skills/deck-add-slide/SKILL.md +217 -217
- package/skills/deck-delete-slide/SKILL.md +51 -51
- package/skills/deck-generate-image/SKILL.md +85 -85
- package/skills/deck-inspect/SKILL.md +60 -60
- package/skills/deck-sketch/SKILL.md +91 -91
- package/skills/deck-validate-project/SKILL.md +80 -80
- package/slides/GenericThankYouSlide.jsx +31 -31
- package/slides/ThankYouSlide.module.css +131 -131
- package/styles/global.css +191 -191
- package/vite.js +26 -26
package/scripts/export-pdf.mjs
CHANGED
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Export slides to PDF using Puppeteer + embedded Vite dev server.
|
|
4
|
-
*
|
|
5
|
-
* For project exports (--project), the script spins up its own Vite dev
|
|
6
|
-
* server so no separate `npm run dev` is needed.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node scripts/export-pdf.mjs --project dev-plan
|
|
10
|
-
* node scripts/export-pdf.mjs # ghcp slides (needs running dev server)
|
|
11
|
-
* node scripts/export-pdf.mjs --customer --customer-name Rabobank --from 12 --to 19
|
|
12
|
-
* node scripts/export-pdf.mjs --internal --from 2 --to 10
|
|
13
|
-
*/
|
|
14
|
-
import puppeteer from 'puppeteer'
|
|
15
|
-
import { existsSync, mkdirSync } from 'fs'
|
|
16
|
-
import path from 'path'
|
|
17
|
-
import { createServer } from 'vite'
|
|
18
|
-
|
|
19
|
-
const args = process.argv.slice(2)
|
|
20
|
-
const flag = (name) => args.includes(`--${name}`)
|
|
21
|
-
const getArg = (name, fallback) => {
|
|
22
|
-
const idx = args.indexOf(`--${name}`)
|
|
23
|
-
return idx !== -1 && args[idx + 1] ? Number(args[idx + 1]) : fallback
|
|
24
|
-
}
|
|
25
|
-
const getStringArg = (name, fallback) => {
|
|
26
|
-
const idx = args.indexOf(`--${name}`)
|
|
27
|
-
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const ROOT = path.resolve(getStringArg('root', process.cwd()))
|
|
31
|
-
const outDir = path.resolve(ROOT, getStringArg('out-dir', 'exports'))
|
|
32
|
-
const viteConfigPath = path.resolve(ROOT, getStringArg('config', 'vite.config.js'))
|
|
33
|
-
|
|
34
|
-
const PORT = getArg('port', 5173)
|
|
35
|
-
const FROM = getArg('from', null)
|
|
36
|
-
const TO = getArg('to', null)
|
|
37
|
-
const CUSTOMER_NAME = getStringArg('customer-name', null)
|
|
38
|
-
const PROJECT = getStringArg('project', null)
|
|
39
|
-
const isCustomer = flag('customer')
|
|
40
|
-
const slug = (s) => s.toLowerCase().replace(/\s+/g, '-')
|
|
41
|
-
const outFile = PROJECT
|
|
42
|
-
? (CUSTOMER_NAME
|
|
43
|
-
? `${slug(CUSTOMER_NAME)}-slides.pdf`
|
|
44
|
-
: `${PROJECT}-slides.pdf`)
|
|
45
|
-
: isCustomer
|
|
46
|
-
? `${slug(CUSTOMER_NAME || 'customer')}-slides.pdf`
|
|
47
|
-
: flag('internal') ? 'internal-slides.pdf' : 'slides.pdf'
|
|
48
|
-
|
|
49
|
-
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })
|
|
50
|
-
|
|
51
|
-
async function hideNav(page) {
|
|
52
|
-
await page.evaluate(() => {
|
|
53
|
-
document.querySelectorAll(
|
|
54
|
-
'[class*="nav"], [class*="Nav"], [class*="progress"], ' +
|
|
55
|
-
'[class*="Progress"], [class*="hint"], [class*="Hint"]'
|
|
56
|
-
).forEach(el => {
|
|
57
|
-
if (el.tagName !== 'BODY' && el.tagName !== 'HTML') {
|
|
58
|
-
el.style.display = 'none'
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
65
|
-
|
|
66
|
-
async function goToSlide(page, targetIdx, totalSlides) {
|
|
67
|
-
for (let i = 0; i < totalSlides; i++) {
|
|
68
|
-
await page.keyboard.press('ArrowLeft')
|
|
69
|
-
await sleep(50)
|
|
70
|
-
}
|
|
71
|
-
for (let i = 0; i < targetIdx; i++) {
|
|
72
|
-
await page.keyboard.press('ArrowRight')
|
|
73
|
-
await sleep(100)
|
|
74
|
-
}
|
|
75
|
-
await sleep(800)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function captureSlides(page, fromIdx, toIdx, totalSlides) {
|
|
79
|
-
await goToSlide(page, fromIdx, totalSlides)
|
|
80
|
-
const shots = []
|
|
81
|
-
for (let i = fromIdx; i <= toIdx; i++) {
|
|
82
|
-
console.log(` šø Slide ${i + 1}/${totalSlides}`)
|
|
83
|
-
await sleep(400)
|
|
84
|
-
shots.push(await page.screenshot({ type: 'png', encoding: 'binary' }))
|
|
85
|
-
if (i < toIdx) {
|
|
86
|
-
await page.keyboard.press('ArrowRight')
|
|
87
|
-
await sleep(700)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return shots
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function buildPDF(browser, screenshots, pdfPath) {
|
|
94
|
-
const pdfPage = await browser.newPage()
|
|
95
|
-
await pdfPage.setViewport({ width: 1920, height: 1080 })
|
|
96
|
-
|
|
97
|
-
const imgTags = screenshots.map(buf => {
|
|
98
|
-
const b64 = Buffer.from(buf).toString('base64')
|
|
99
|
-
return `<div class="page"><img src="data:image/png;base64,${b64}" /></div>`
|
|
100
|
-
}).join('\n')
|
|
101
|
-
|
|
102
|
-
await pdfPage.setContent(`<!DOCTYPE html>
|
|
103
|
-
<html><head><style>
|
|
104
|
-
*{margin:0;padding:0;box-sizing:border-box}
|
|
105
|
-
html,body{width:1920px;overflow:hidden}
|
|
106
|
-
@page{size:1920px 1080px;margin:0}
|
|
107
|
-
.page{width:1920px;height:1080px;page-break-after:always;page-break-inside:avoid;overflow:hidden}
|
|
108
|
-
.page:last-child{page-break-after:avoid}
|
|
109
|
-
.page img{display:block;width:1920px;height:1080px;object-fit:fill}
|
|
110
|
-
</style></head><body>${imgTags}</body></html>`, { waitUntil: 'load' })
|
|
111
|
-
|
|
112
|
-
await pdfPage.pdf({
|
|
113
|
-
path: pdfPath,
|
|
114
|
-
width: '1920px',
|
|
115
|
-
height: '1080px',
|
|
116
|
-
printBackground: true,
|
|
117
|
-
preferCSSPageSize: true,
|
|
118
|
-
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
|
|
119
|
-
})
|
|
120
|
-
await pdfPage.close()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function exportProjectPDF() {
|
|
124
|
-
console.log(`\nš Exporting project: ${PROJECT}`)
|
|
125
|
-
|
|
126
|
-
const server = await createServer({
|
|
127
|
-
root: ROOT,
|
|
128
|
-
configFile: viteConfigPath,
|
|
129
|
-
server: { port: 0, strictPort: false, host: '127.0.0.1' },
|
|
130
|
-
logLevel: 'silent',
|
|
131
|
-
})
|
|
132
|
-
await server.listen()
|
|
133
|
-
const addr = server.httpServer.address()
|
|
134
|
-
const base = `http://127.0.0.1:${addr.port}`
|
|
135
|
-
console.log(` Vite server listening on ${base}`)
|
|
136
|
-
|
|
137
|
-
await sleep(2000)
|
|
138
|
-
|
|
139
|
-
const browser = await puppeteer.launch({
|
|
140
|
-
headless: true,
|
|
141
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
142
|
-
})
|
|
143
|
-
const page = await browser.newPage()
|
|
144
|
-
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 2 })
|
|
145
|
-
|
|
146
|
-
const url = `${base}/#/${PROJECT}`
|
|
147
|
-
console.log(`ā³ Loading ${url}`)
|
|
148
|
-
let loaded = false
|
|
149
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
150
|
-
try {
|
|
151
|
-
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
152
|
-
loaded = true
|
|
153
|
-
break
|
|
154
|
-
} catch (err) {
|
|
155
|
-
console.log(` Attempt ${attempt}/3 failed: ${err.message}`)
|
|
156
|
-
await sleep(2000)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (!loaded) throw new Error('Could not load the Vite dev server after 3 attempts')
|
|
160
|
-
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
161
|
-
|
|
162
|
-
await page.waitForFunction(
|
|
163
|
-
() => document.querySelectorAll('.slide').length > 0,
|
|
164
|
-
{ timeout: 15000 }
|
|
165
|
-
)
|
|
166
|
-
await sleep(1500)
|
|
167
|
-
|
|
168
|
-
await hideNav(page)
|
|
169
|
-
|
|
170
|
-
if (CUSTOMER_NAME) {
|
|
171
|
-
console.log(`š¤ Selecting customer: ${CUSTOMER_NAME}`)
|
|
172
|
-
const clicked = await page.evaluate(() => {
|
|
173
|
-
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.includes('Customer Facing'))
|
|
174
|
-
if (btn) {
|
|
175
|
-
btn.click()
|
|
176
|
-
return true
|
|
177
|
-
}
|
|
178
|
-
return false
|
|
179
|
-
})
|
|
180
|
-
if (!clicked) {
|
|
181
|
-
console.error('ā "Customer Facing" button not found')
|
|
182
|
-
await browser.close()
|
|
183
|
-
await server.close()
|
|
184
|
-
process.exit(1)
|
|
185
|
-
}
|
|
186
|
-
await sleep(800)
|
|
187
|
-
|
|
188
|
-
const picked = await page.evaluate(name => {
|
|
189
|
-
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim().includes(name))
|
|
190
|
-
if (btn) {
|
|
191
|
-
btn.click()
|
|
192
|
-
return true
|
|
193
|
-
}
|
|
194
|
-
return false
|
|
195
|
-
}, CUSTOMER_NAME)
|
|
196
|
-
if (!picked) {
|
|
197
|
-
console.error(`ā Customer "${CUSTOMER_NAME}" not found`)
|
|
198
|
-
await browser.close()
|
|
199
|
-
await server.close()
|
|
200
|
-
process.exit(1)
|
|
201
|
-
}
|
|
202
|
-
await sleep(1000)
|
|
203
|
-
console.log('ā
Customer selected')
|
|
204
|
-
await hideNav(page)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const totalSlides = await page.evaluate(() => document.querySelectorAll('.slide').length)
|
|
208
|
-
const fromIdx = FROM ? FROM - 1 : 0
|
|
209
|
-
const toIdx = TO ? TO - 1 : totalSlides - 1
|
|
210
|
-
console.log(`š ${totalSlides} slides ā exporting ${fromIdx + 1}ā${toIdx + 1}`)
|
|
211
|
-
|
|
212
|
-
const screenshots = await captureSlides(page, fromIdx, toIdx, totalSlides)
|
|
213
|
-
|
|
214
|
-
const pdfPath = path.join(outDir, outFile)
|
|
215
|
-
await buildPDF(browser, screenshots, pdfPath)
|
|
216
|
-
|
|
217
|
-
await browser.close()
|
|
218
|
-
await server.close()
|
|
219
|
-
console.log(`\nā
PDF saved to ${pdfPath}`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function exportDevServerPDF() {
|
|
223
|
-
const BASE = `http://localhost:${PORT}`
|
|
224
|
-
console.log(`\nā³ Connecting to dev server at ${BASE}`)
|
|
225
|
-
|
|
226
|
-
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
|
|
227
|
-
const page = await browser.newPage()
|
|
228
|
-
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 2 })
|
|
229
|
-
|
|
230
|
-
await page.goto(`${BASE}/#/ghcp`, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
231
|
-
await page.waitForSelector('.slide.active', { timeout: 10000 })
|
|
232
|
-
|
|
233
|
-
if (isCustomer && CUSTOMER_NAME) {
|
|
234
|
-
console.log(`š¤ Selecting customer: ${CUSTOMER_NAME}`)
|
|
235
|
-
const clicked = await page.evaluate(() => {
|
|
236
|
-
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.includes('Customer Facing'))
|
|
237
|
-
if (btn) {
|
|
238
|
-
btn.click()
|
|
239
|
-
return true
|
|
240
|
-
}
|
|
241
|
-
return false
|
|
242
|
-
})
|
|
243
|
-
if (!clicked) {
|
|
244
|
-
console.error('ā "Customer Facing" button not found')
|
|
245
|
-
await browser.close()
|
|
246
|
-
process.exit(1)
|
|
247
|
-
}
|
|
248
|
-
await sleep(800)
|
|
249
|
-
|
|
250
|
-
const picked = await page.evaluate(name => {
|
|
251
|
-
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim().includes(name))
|
|
252
|
-
if (btn) {
|
|
253
|
-
btn.click()
|
|
254
|
-
return true
|
|
255
|
-
}
|
|
256
|
-
return false
|
|
257
|
-
}, CUSTOMER_NAME)
|
|
258
|
-
if (!picked) {
|
|
259
|
-
console.error(`ā Customer "${CUSTOMER_NAME}" not found`)
|
|
260
|
-
await browser.close()
|
|
261
|
-
process.exit(1)
|
|
262
|
-
}
|
|
263
|
-
await sleep(1000)
|
|
264
|
-
console.log('ā
Customer selected')
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
await hideNav(page)
|
|
268
|
-
|
|
269
|
-
const totalSlides = await page.evaluate(() => document.querySelectorAll('.slide').length)
|
|
270
|
-
const fromIdx = FROM ? FROM - 1 : 0
|
|
271
|
-
const toIdx = TO ? TO - 1 : totalSlides - 1
|
|
272
|
-
console.log(`š ${totalSlides} slides ā exporting ${fromIdx + 1}ā${toIdx + 1}`)
|
|
273
|
-
|
|
274
|
-
const screenshots = await captureSlides(page, fromIdx, toIdx, totalSlides)
|
|
275
|
-
|
|
276
|
-
const pdfPath = path.join(outDir, outFile)
|
|
277
|
-
await buildPDF(browser, screenshots, pdfPath)
|
|
278
|
-
|
|
279
|
-
await browser.close()
|
|
280
|
-
console.log(`\nā
PDF saved to ${pdfPath}`)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const run = PROJECT ? exportProjectPDF : exportDevServerPDF
|
|
284
|
-
run().catch(err => {
|
|
285
|
-
console.error('ā Export failed:', err.message)
|
|
286
|
-
process.exit(1)
|
|
287
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Export slides to PDF using Puppeteer + embedded Vite dev server.
|
|
4
|
+
*
|
|
5
|
+
* For project exports (--project), the script spins up its own Vite dev
|
|
6
|
+
* server so no separate `npm run dev` is needed.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/export-pdf.mjs --project dev-plan
|
|
10
|
+
* node scripts/export-pdf.mjs # ghcp slides (needs running dev server)
|
|
11
|
+
* node scripts/export-pdf.mjs --customer --customer-name Rabobank --from 12 --to 19
|
|
12
|
+
* node scripts/export-pdf.mjs --internal --from 2 --to 10
|
|
13
|
+
*/
|
|
14
|
+
import puppeteer from 'puppeteer'
|
|
15
|
+
import { existsSync, mkdirSync } from 'fs'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
import { createServer } from 'vite'
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2)
|
|
20
|
+
const flag = (name) => args.includes(`--${name}`)
|
|
21
|
+
const getArg = (name, fallback) => {
|
|
22
|
+
const idx = args.indexOf(`--${name}`)
|
|
23
|
+
return idx !== -1 && args[idx + 1] ? Number(args[idx + 1]) : fallback
|
|
24
|
+
}
|
|
25
|
+
const getStringArg = (name, fallback) => {
|
|
26
|
+
const idx = args.indexOf(`--${name}`)
|
|
27
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(getStringArg('root', process.cwd()))
|
|
31
|
+
const outDir = path.resolve(ROOT, getStringArg('out-dir', 'exports'))
|
|
32
|
+
const viteConfigPath = path.resolve(ROOT, getStringArg('config', 'vite.config.js'))
|
|
33
|
+
|
|
34
|
+
const PORT = getArg('port', 5173)
|
|
35
|
+
const FROM = getArg('from', null)
|
|
36
|
+
const TO = getArg('to', null)
|
|
37
|
+
const CUSTOMER_NAME = getStringArg('customer-name', null)
|
|
38
|
+
const PROJECT = getStringArg('project', null)
|
|
39
|
+
const isCustomer = flag('customer')
|
|
40
|
+
const slug = (s) => s.toLowerCase().replace(/\s+/g, '-')
|
|
41
|
+
const outFile = PROJECT
|
|
42
|
+
? (CUSTOMER_NAME
|
|
43
|
+
? `${slug(CUSTOMER_NAME)}-slides.pdf`
|
|
44
|
+
: `${PROJECT}-slides.pdf`)
|
|
45
|
+
: isCustomer
|
|
46
|
+
? `${slug(CUSTOMER_NAME || 'customer')}-slides.pdf`
|
|
47
|
+
: flag('internal') ? 'internal-slides.pdf' : 'slides.pdf'
|
|
48
|
+
|
|
49
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })
|
|
50
|
+
|
|
51
|
+
async function hideNav(page) {
|
|
52
|
+
await page.evaluate(() => {
|
|
53
|
+
document.querySelectorAll(
|
|
54
|
+
'[class*="nav"], [class*="Nav"], [class*="progress"], ' +
|
|
55
|
+
'[class*="Progress"], [class*="hint"], [class*="Hint"]'
|
|
56
|
+
).forEach(el => {
|
|
57
|
+
if (el.tagName !== 'BODY' && el.tagName !== 'HTML') {
|
|
58
|
+
el.style.display = 'none'
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
65
|
+
|
|
66
|
+
async function goToSlide(page, targetIdx, totalSlides) {
|
|
67
|
+
for (let i = 0; i < totalSlides; i++) {
|
|
68
|
+
await page.keyboard.press('ArrowLeft')
|
|
69
|
+
await sleep(50)
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i < targetIdx; i++) {
|
|
72
|
+
await page.keyboard.press('ArrowRight')
|
|
73
|
+
await sleep(100)
|
|
74
|
+
}
|
|
75
|
+
await sleep(800)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function captureSlides(page, fromIdx, toIdx, totalSlides) {
|
|
79
|
+
await goToSlide(page, fromIdx, totalSlides)
|
|
80
|
+
const shots = []
|
|
81
|
+
for (let i = fromIdx; i <= toIdx; i++) {
|
|
82
|
+
console.log(` šø Slide ${i + 1}/${totalSlides}`)
|
|
83
|
+
await sleep(400)
|
|
84
|
+
shots.push(await page.screenshot({ type: 'png', encoding: 'binary' }))
|
|
85
|
+
if (i < toIdx) {
|
|
86
|
+
await page.keyboard.press('ArrowRight')
|
|
87
|
+
await sleep(700)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return shots
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function buildPDF(browser, screenshots, pdfPath) {
|
|
94
|
+
const pdfPage = await browser.newPage()
|
|
95
|
+
await pdfPage.setViewport({ width: 1920, height: 1080 })
|
|
96
|
+
|
|
97
|
+
const imgTags = screenshots.map(buf => {
|
|
98
|
+
const b64 = Buffer.from(buf).toString('base64')
|
|
99
|
+
return `<div class="page"><img src="data:image/png;base64,${b64}" /></div>`
|
|
100
|
+
}).join('\n')
|
|
101
|
+
|
|
102
|
+
await pdfPage.setContent(`<!DOCTYPE html>
|
|
103
|
+
<html><head><style>
|
|
104
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
105
|
+
html,body{width:1920px;overflow:hidden}
|
|
106
|
+
@page{size:1920px 1080px;margin:0}
|
|
107
|
+
.page{width:1920px;height:1080px;page-break-after:always;page-break-inside:avoid;overflow:hidden}
|
|
108
|
+
.page:last-child{page-break-after:avoid}
|
|
109
|
+
.page img{display:block;width:1920px;height:1080px;object-fit:fill}
|
|
110
|
+
</style></head><body>${imgTags}</body></html>`, { waitUntil: 'load' })
|
|
111
|
+
|
|
112
|
+
await pdfPage.pdf({
|
|
113
|
+
path: pdfPath,
|
|
114
|
+
width: '1920px',
|
|
115
|
+
height: '1080px',
|
|
116
|
+
printBackground: true,
|
|
117
|
+
preferCSSPageSize: true,
|
|
118
|
+
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
|
|
119
|
+
})
|
|
120
|
+
await pdfPage.close()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function exportProjectPDF() {
|
|
124
|
+
console.log(`\nš Exporting project: ${PROJECT}`)
|
|
125
|
+
|
|
126
|
+
const server = await createServer({
|
|
127
|
+
root: ROOT,
|
|
128
|
+
configFile: viteConfigPath,
|
|
129
|
+
server: { port: 0, strictPort: false, host: '127.0.0.1' },
|
|
130
|
+
logLevel: 'silent',
|
|
131
|
+
})
|
|
132
|
+
await server.listen()
|
|
133
|
+
const addr = server.httpServer.address()
|
|
134
|
+
const base = `http://127.0.0.1:${addr.port}`
|
|
135
|
+
console.log(` Vite server listening on ${base}`)
|
|
136
|
+
|
|
137
|
+
await sleep(2000)
|
|
138
|
+
|
|
139
|
+
const browser = await puppeteer.launch({
|
|
140
|
+
headless: true,
|
|
141
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
142
|
+
})
|
|
143
|
+
const page = await browser.newPage()
|
|
144
|
+
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 2 })
|
|
145
|
+
|
|
146
|
+
const url = `${base}/#/${PROJECT}`
|
|
147
|
+
console.log(`ā³ Loading ${url}`)
|
|
148
|
+
let loaded = false
|
|
149
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
150
|
+
try {
|
|
151
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
152
|
+
loaded = true
|
|
153
|
+
break
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(` Attempt ${attempt}/3 failed: ${err.message}`)
|
|
156
|
+
await sleep(2000)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!loaded) throw new Error('Could not load the Vite dev server after 3 attempts')
|
|
160
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
161
|
+
|
|
162
|
+
await page.waitForFunction(
|
|
163
|
+
() => document.querySelectorAll('.slide').length > 0,
|
|
164
|
+
{ timeout: 15000 }
|
|
165
|
+
)
|
|
166
|
+
await sleep(1500)
|
|
167
|
+
|
|
168
|
+
await hideNav(page)
|
|
169
|
+
|
|
170
|
+
if (CUSTOMER_NAME) {
|
|
171
|
+
console.log(`š¤ Selecting customer: ${CUSTOMER_NAME}`)
|
|
172
|
+
const clicked = await page.evaluate(() => {
|
|
173
|
+
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.includes('Customer Facing'))
|
|
174
|
+
if (btn) {
|
|
175
|
+
btn.click()
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
return false
|
|
179
|
+
})
|
|
180
|
+
if (!clicked) {
|
|
181
|
+
console.error('ā "Customer Facing" button not found')
|
|
182
|
+
await browser.close()
|
|
183
|
+
await server.close()
|
|
184
|
+
process.exit(1)
|
|
185
|
+
}
|
|
186
|
+
await sleep(800)
|
|
187
|
+
|
|
188
|
+
const picked = await page.evaluate(name => {
|
|
189
|
+
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim().includes(name))
|
|
190
|
+
if (btn) {
|
|
191
|
+
btn.click()
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
return false
|
|
195
|
+
}, CUSTOMER_NAME)
|
|
196
|
+
if (!picked) {
|
|
197
|
+
console.error(`ā Customer "${CUSTOMER_NAME}" not found`)
|
|
198
|
+
await browser.close()
|
|
199
|
+
await server.close()
|
|
200
|
+
process.exit(1)
|
|
201
|
+
}
|
|
202
|
+
await sleep(1000)
|
|
203
|
+
console.log('ā
Customer selected')
|
|
204
|
+
await hideNav(page)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const totalSlides = await page.evaluate(() => document.querySelectorAll('.slide').length)
|
|
208
|
+
const fromIdx = FROM ? FROM - 1 : 0
|
|
209
|
+
const toIdx = TO ? TO - 1 : totalSlides - 1
|
|
210
|
+
console.log(`š ${totalSlides} slides ā exporting ${fromIdx + 1}ā${toIdx + 1}`)
|
|
211
|
+
|
|
212
|
+
const screenshots = await captureSlides(page, fromIdx, toIdx, totalSlides)
|
|
213
|
+
|
|
214
|
+
const pdfPath = path.join(outDir, outFile)
|
|
215
|
+
await buildPDF(browser, screenshots, pdfPath)
|
|
216
|
+
|
|
217
|
+
await browser.close()
|
|
218
|
+
await server.close()
|
|
219
|
+
console.log(`\nā
PDF saved to ${pdfPath}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function exportDevServerPDF() {
|
|
223
|
+
const BASE = `http://localhost:${PORT}`
|
|
224
|
+
console.log(`\nā³ Connecting to dev server at ${BASE}`)
|
|
225
|
+
|
|
226
|
+
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
|
|
227
|
+
const page = await browser.newPage()
|
|
228
|
+
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 2 })
|
|
229
|
+
|
|
230
|
+
await page.goto(`${BASE}/#/ghcp`, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
231
|
+
await page.waitForSelector('.slide.active', { timeout: 10000 })
|
|
232
|
+
|
|
233
|
+
if (isCustomer && CUSTOMER_NAME) {
|
|
234
|
+
console.log(`š¤ Selecting customer: ${CUSTOMER_NAME}`)
|
|
235
|
+
const clicked = await page.evaluate(() => {
|
|
236
|
+
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.includes('Customer Facing'))
|
|
237
|
+
if (btn) {
|
|
238
|
+
btn.click()
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
return false
|
|
242
|
+
})
|
|
243
|
+
if (!clicked) {
|
|
244
|
+
console.error('ā "Customer Facing" button not found')
|
|
245
|
+
await browser.close()
|
|
246
|
+
process.exit(1)
|
|
247
|
+
}
|
|
248
|
+
await sleep(800)
|
|
249
|
+
|
|
250
|
+
const picked = await page.evaluate(name => {
|
|
251
|
+
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim().includes(name))
|
|
252
|
+
if (btn) {
|
|
253
|
+
btn.click()
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
return false
|
|
257
|
+
}, CUSTOMER_NAME)
|
|
258
|
+
if (!picked) {
|
|
259
|
+
console.error(`ā Customer "${CUSTOMER_NAME}" not found`)
|
|
260
|
+
await browser.close()
|
|
261
|
+
process.exit(1)
|
|
262
|
+
}
|
|
263
|
+
await sleep(1000)
|
|
264
|
+
console.log('ā
Customer selected')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await hideNav(page)
|
|
268
|
+
|
|
269
|
+
const totalSlides = await page.evaluate(() => document.querySelectorAll('.slide').length)
|
|
270
|
+
const fromIdx = FROM ? FROM - 1 : 0
|
|
271
|
+
const toIdx = TO ? TO - 1 : totalSlides - 1
|
|
272
|
+
console.log(`š ${totalSlides} slides ā exporting ${fromIdx + 1}ā${toIdx + 1}`)
|
|
273
|
+
|
|
274
|
+
const screenshots = await captureSlides(page, fromIdx, toIdx, totalSlides)
|
|
275
|
+
|
|
276
|
+
const pdfPath = path.join(outDir, outFile)
|
|
277
|
+
await buildPDF(browser, screenshots, pdfPath)
|
|
278
|
+
|
|
279
|
+
await browser.close()
|
|
280
|
+
console.log(`\nā
PDF saved to ${pdfPath}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const run = PROJECT ? exportProjectPDF : exportDevServerPDF
|
|
284
|
+
run().catch(err => {
|
|
285
|
+
console.error('ā Export failed:', err.message)
|
|
286
|
+
process.exit(1)
|
|
287
|
+
})
|