@deckio/deck-engine 1.8.1 → 1.8.2
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.
|
@@ -2,6 +2,7 @@ import { useSlides } from '../context/SlideContext'
|
|
|
2
2
|
import styles from './Navigation.module.css'
|
|
3
3
|
import { useState, useEffect, useRef } from 'react'
|
|
4
4
|
import { exportDeckPdf } from './exportDeckPdf.js'
|
|
5
|
+
import { exportDeckPptx } from './exportDeckPptx.js'
|
|
5
6
|
|
|
6
7
|
function resolveProp(value, context) {
|
|
7
8
|
return typeof value === 'function' ? value(context) : value
|
|
@@ -13,6 +14,8 @@ export default function Navigation({ pdfPath = null, pdfLabel = 'Deck PDF' }) {
|
|
|
13
14
|
const [idle, setIdle] = useState(false)
|
|
14
15
|
const [isExporting, setIsExporting] = useState(false)
|
|
15
16
|
const [exportStatus, setExportStatus] = useState('PDF')
|
|
17
|
+
const [exportMenuOpen, setExportMenuOpen] = useState(false)
|
|
18
|
+
const exportMenuRef = useRef(null)
|
|
16
19
|
const timerRef = useRef(null)
|
|
17
20
|
|
|
18
21
|
useEffect(() => {
|
|
@@ -36,19 +39,34 @@ export default function Navigation({ pdfPath = null, pdfLabel = 'Deck PDF' }) {
|
|
|
36
39
|
}
|
|
37
40
|
}, [])
|
|
38
41
|
|
|
42
|
+
// Close export menu when clicking outside
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!exportMenuOpen) return
|
|
45
|
+
const handleClickOutside = (e) => {
|
|
46
|
+
if (exportMenuRef.current && !exportMenuRef.current.contains(e.target)) {
|
|
47
|
+
setExportMenuOpen(false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
51
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
52
|
+
}, [exportMenuOpen])
|
|
53
|
+
|
|
39
54
|
const progress = ((current + 1) / totalSlides) * 100
|
|
40
55
|
const navigationState = { current, totalSlides, selectedCustomer, project }
|
|
41
56
|
const resolvedPdfPath = resolveProp(pdfPath, navigationState)
|
|
42
57
|
const resolvedPdfLabel = resolveProp(pdfLabel, navigationState) || 'Deck PDF'
|
|
43
58
|
|
|
44
|
-
async function
|
|
45
|
-
if (
|
|
46
|
-
|
|
59
|
+
async function handleExport(format) {
|
|
60
|
+
if (isExporting) return
|
|
61
|
+
setExportMenuOpen(false)
|
|
47
62
|
setIsExporting(true)
|
|
48
63
|
setExportStatus('Preparing')
|
|
49
64
|
|
|
65
|
+
const exportFn = format === 'pptx' ? exportDeckPptx : exportDeckPdf
|
|
66
|
+
const label = format === 'pptx' ? 'PPTX' : 'PDF'
|
|
67
|
+
|
|
50
68
|
try {
|
|
51
|
-
await
|
|
69
|
+
await exportFn({
|
|
52
70
|
current,
|
|
53
71
|
goTo,
|
|
54
72
|
project,
|
|
@@ -60,7 +78,7 @@ export default function Navigation({ pdfPath = null, pdfLabel = 'Deck PDF' }) {
|
|
|
60
78
|
})
|
|
61
79
|
setExportStatus('Done')
|
|
62
80
|
} catch (error) {
|
|
63
|
-
console.error(
|
|
81
|
+
console.error(`${label} export failed`, error)
|
|
64
82
|
setExportStatus('Error')
|
|
65
83
|
} finally {
|
|
66
84
|
window.setTimeout(() => {
|
|
@@ -89,41 +107,63 @@ export default function Navigation({ pdfPath = null, pdfLabel = 'Deck PDF' }) {
|
|
|
89
107
|
</button>
|
|
90
108
|
)}
|
|
91
109
|
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
<div className={styles.exportGroup} ref={exportMenuRef}>
|
|
111
|
+
{resolvedPdfPath ? (
|
|
112
|
+
<a
|
|
113
|
+
className={styles.exportBtn}
|
|
114
|
+
href={resolvedPdfPath}
|
|
115
|
+
target="_blank"
|
|
116
|
+
rel="noopener noreferrer"
|
|
117
|
+
title={resolvedPdfLabel}
|
|
118
|
+
>
|
|
119
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
120
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
121
|
+
<polyline points="14 2 14 8 20 8" />
|
|
122
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
123
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
124
|
+
<polyline points="10 9 9 9 8 9" />
|
|
125
|
+
</svg>
|
|
126
|
+
<span className={styles.exportLabel}>PDF</span>
|
|
127
|
+
</a>
|
|
128
|
+
) : (
|
|
129
|
+
<button
|
|
130
|
+
className={`${styles.exportBtn} ${isExporting ? styles.exportBtnBusy : ''}`}
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={() => isExporting ? null : setExportMenuOpen(!exportMenuOpen)}
|
|
133
|
+
disabled={isExporting}
|
|
134
|
+
title={isExporting ? 'Exporting...' : 'Export deck'}
|
|
135
|
+
>
|
|
136
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
137
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
138
|
+
<polyline points="14 2 14 8 20 8" />
|
|
139
|
+
<path d="M12 12v6" />
|
|
140
|
+
<path d="M9 15l3 3 3-3" />
|
|
141
|
+
<path d="M8 10h8" />
|
|
142
|
+
</svg>
|
|
143
|
+
<span className={styles.exportLabel}>{isExporting ? exportStatus : '⬇'}</span>
|
|
144
|
+
</button>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{exportMenuOpen && !isExporting && (
|
|
148
|
+
<div className={styles.exportMenu}>
|
|
149
|
+
<button className={styles.exportMenuItem} onClick={() => handleExport('pdf')}>
|
|
150
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="14" height="14">
|
|
151
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
152
|
+
<polyline points="14 2 14 8 20 8" />
|
|
153
|
+
</svg>
|
|
154
|
+
PDF
|
|
155
|
+
</button>
|
|
156
|
+
<button className={styles.exportMenuItem} onClick={() => handleExport('pptx')}>
|
|
157
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="14" height="14">
|
|
158
|
+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
|
159
|
+
<path d="M8 21h8" />
|
|
160
|
+
<path d="M12 17v4" />
|
|
161
|
+
</svg>
|
|
162
|
+
PowerPoint
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
127
167
|
|
|
128
168
|
<button
|
|
129
169
|
className={`${styles.navBtn} ${styles.prev}`}
|
|
@@ -98,6 +98,62 @@
|
|
|
98
98
|
text-transform: uppercase;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
.exportGroup {
|
|
102
|
+
position: fixed;
|
|
103
|
+
top: 16px;
|
|
104
|
+
right: 20px;
|
|
105
|
+
z-index: 200;
|
|
106
|
+
}
|
|
107
|
+
.exportGroup .exportBtn {
|
|
108
|
+
position: static;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.exportMenu {
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: calc(100% + 6px);
|
|
114
|
+
right: 0;
|
|
115
|
+
min-width: 140px;
|
|
116
|
+
border-radius: 10px;
|
|
117
|
+
border: 1px solid var(--border);
|
|
118
|
+
background: rgba(22,27,34,0.95);
|
|
119
|
+
backdrop-filter: blur(12px);
|
|
120
|
+
padding: 4px;
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: 2px;
|
|
124
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
125
|
+
animation: menuFadeIn 0.15s ease;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@keyframes menuFadeIn {
|
|
129
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
130
|
+
to { opacity: 1; transform: translateY(0); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.exportMenuItem {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 8px;
|
|
137
|
+
padding: 8px 12px;
|
|
138
|
+
border: none;
|
|
139
|
+
border-radius: 6px;
|
|
140
|
+
background: transparent;
|
|
141
|
+
color: var(--text);
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
font-family: inherit;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
transition: background 0.15s ease;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
.exportMenuItem:hover {
|
|
150
|
+
background: rgba(31,111,235,0.2);
|
|
151
|
+
}
|
|
152
|
+
.exportMenuItem svg {
|
|
153
|
+
width: 14px; height: 14px;
|
|
154
|
+
flex-shrink: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
101
157
|
.navBtn {
|
|
102
158
|
position: fixed;
|
|
103
159
|
top: 50%;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export deck slides to PowerPoint (.pptx) — direct download, no dialogs.
|
|
3
|
+
*
|
|
4
|
+
* Uses modern-screenshot (SVG foreignObject) + PptxGenJS.
|
|
5
|
+
* Each slide is captured as a full-bleed image placed on a 10×5.625″ slide (16:9).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PAGE_W = 1920
|
|
9
|
+
const PAGE_H = 1080
|
|
10
|
+
const SETTLE_MS = 600
|
|
11
|
+
|
|
12
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
13
|
+
|
|
14
|
+
async function waitForPaint() {
|
|
15
|
+
await new Promise((r) => requestAnimationFrame(() => r()))
|
|
16
|
+
await new Promise((r) => requestAnimationFrame(() => r()))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sanitize(v) {
|
|
20
|
+
return String(v || 'deck').trim().toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'deck'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildFileName({ project, selectedCustomer }) {
|
|
25
|
+
const base = selectedCustomer
|
|
26
|
+
? `${selectedCustomer} ${document.title || project || 'deck'}`
|
|
27
|
+
: document.title || project || 'deck'
|
|
28
|
+
return `${sanitize(base)}.pptx`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pauseAnimations(slide) {
|
|
32
|
+
const undo = []
|
|
33
|
+
const pause = (el) => {
|
|
34
|
+
const orig = el.style.animationPlayState
|
|
35
|
+
el.style.animationPlayState = 'paused'
|
|
36
|
+
undo.push(() => { el.style.animationPlayState = orig })
|
|
37
|
+
}
|
|
38
|
+
pause(slide)
|
|
39
|
+
slide.querySelectorAll('*').forEach(pause)
|
|
40
|
+
return () => { for (let i = undo.length - 1; i >= 0; i--) undo[i]() }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function exportDeckPptx({
|
|
44
|
+
current,
|
|
45
|
+
goTo,
|
|
46
|
+
project,
|
|
47
|
+
selectedCustomer,
|
|
48
|
+
totalSlides,
|
|
49
|
+
onProgress,
|
|
50
|
+
}) {
|
|
51
|
+
const deck = document.querySelector('.deck')
|
|
52
|
+
const slides = Array.from(deck?.querySelectorAll('.slide') || [])
|
|
53
|
+
if (!deck || slides.length === 0) throw new Error('No slides found')
|
|
54
|
+
|
|
55
|
+
const [{ domToPng }, PptxGenJS] = await Promise.all([
|
|
56
|
+
import('modern-screenshot'),
|
|
57
|
+
import('pptxgenjs'),
|
|
58
|
+
])
|
|
59
|
+
const Pptx = PptxGenJS.default || PptxGenJS
|
|
60
|
+
|
|
61
|
+
const bg = getComputedStyle(document.documentElement)
|
|
62
|
+
.getPropertyValue('--bg-deep').trim() || '#080b10'
|
|
63
|
+
const scale = Math.min(window.devicePixelRatio || 1, 2)
|
|
64
|
+
|
|
65
|
+
const pptx = new Pptx()
|
|
66
|
+
pptx.defineLayout({ name: 'WIDE', width: 10, height: 5.625 })
|
|
67
|
+
pptx.layout = 'WIDE'
|
|
68
|
+
|
|
69
|
+
if (document.fonts?.ready) await document.fonts.ready
|
|
70
|
+
|
|
71
|
+
const origDeckCss = deck.style.cssText
|
|
72
|
+
deck.style.width = `${PAGE_W}px`
|
|
73
|
+
deck.style.height = `${PAGE_H}px`
|
|
74
|
+
await waitForPaint()
|
|
75
|
+
await wait(SETTLE_MS)
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
for (let i = 0; i < totalSlides; i++) {
|
|
79
|
+
onProgress?.({ current: i + 1, total: totalSlides })
|
|
80
|
+
goTo(i)
|
|
81
|
+
await waitForPaint()
|
|
82
|
+
await wait(SETTLE_MS)
|
|
83
|
+
|
|
84
|
+
const active = document.querySelector('.slide.active') || slides[i]
|
|
85
|
+
if (!active) throw new Error(`Slide ${i + 1} not found`)
|
|
86
|
+
|
|
87
|
+
const restore = pauseAnimations(active)
|
|
88
|
+
await waitForPaint()
|
|
89
|
+
|
|
90
|
+
let dataUrl
|
|
91
|
+
try {
|
|
92
|
+
dataUrl = await domToPng(active, {
|
|
93
|
+
width: PAGE_W,
|
|
94
|
+
height: PAGE_H,
|
|
95
|
+
backgroundColor: bg,
|
|
96
|
+
scale,
|
|
97
|
+
style: {
|
|
98
|
+
opacity: '1',
|
|
99
|
+
transform: 'none',
|
|
100
|
+
transition: 'none',
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
} finally {
|
|
104
|
+
restore()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const slide = pptx.addSlide()
|
|
108
|
+
slide.background = { color: bg.replace('#', '') }
|
|
109
|
+
slide.addImage({
|
|
110
|
+
data: dataUrl,
|
|
111
|
+
x: 0,
|
|
112
|
+
y: 0,
|
|
113
|
+
w: '100%',
|
|
114
|
+
h: '100%',
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
deck.style.cssText = origDeckCss
|
|
119
|
+
goTo(current)
|
|
120
|
+
await waitForPaint()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const fileName = buildFileName({ project, selectedCustomer })
|
|
124
|
+
await pptx.writeFile({ fileName })
|
|
125
|
+
|
|
126
|
+
return { fileName }
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deckio/deck-engine",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"jspdf": "^3.0.3",
|
|
39
|
-
"modern-screenshot": "^4.6.8"
|
|
39
|
+
"modern-screenshot": "^4.6.8",
|
|
40
|
+
"pptxgenjs": "^3.12.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"react": "^19.1.0",
|