@deckio/deck-engine 1.7.7 → 1.7.8

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.
@@ -121,7 +121,7 @@ export default function Navigation({ pdfPath = null, pdfLabel = 'Deck PDF' }) {
121
121
  <path d="M9 15l3 3 3-3" />
122
122
  <path d="M8 10h8" />
123
123
  </svg>
124
- <span className={styles.exportLabel}>{exportStatus}</span>
124
+ <span className={styles.exportLabel}>{isExporting ? exportStatus : '⬇ PDF'}</span>
125
125
  </button>
126
126
  )}
127
127
 
@@ -1,57 +1,50 @@
1
- const DEFAULT_PAGE_WIDTH = 1920
2
- const DEFAULT_PAGE_HEIGHT = 1080
3
- const EXPORT_TRANSITION_MS = 750
4
-
5
- function wait(ms) {
6
- return new Promise((resolve) => {
7
- window.setTimeout(resolve, ms)
8
- })
9
- }
1
+ /**
2
+ * Export deck slides to PDF — direct download, no dialogs.
3
+ *
4
+ * Uses modern-screenshot (SVG foreignObject) + jspdf.
5
+ * The browser's own renderer handles all CSS natively:
6
+ * - background-clip: text ✅ (explicit fix in modern-screenshot)
7
+ * - filter: blur() ✅ (native foreignObject rendering)
8
+ * - gradients, shadows ✅
9
+ * - animations paused before capture
10
+ */
11
+
12
+ const PAGE_W = 1920
13
+ const PAGE_H = 1080
14
+ const SETTLE_MS = 600
15
+
16
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms))
10
17
 
11
18
  async function waitForPaint() {
12
- await new Promise((resolve) => requestAnimationFrame(() => resolve()))
13
- await new Promise((resolve) => requestAnimationFrame(() => resolve()))
14
- }
15
-
16
- async function waitForFonts() {
17
- if (document.fonts?.ready) {
18
- await document.fonts.ready
19
- }
20
- }
21
-
22
- function sanitizeFileName(value) {
23
- return String(value || 'deck')
24
- .trim()
25
- .toLowerCase()
26
- .replace(/[^a-z0-9]+/g, '-')
27
- .replace(/^-|-$/g, '') || 'deck'
28
- }
29
-
30
- function getDeckElement() {
31
- return document.querySelector('.deck')
19
+ await new Promise((r) => requestAnimationFrame(() => r()))
20
+ await new Promise((r) => requestAnimationFrame(() => r()))
32
21
  }
33
22
 
34
- function getSlideNodes(deckElement) {
35
- return Array.from(deckElement?.querySelectorAll('.slide') || [])
23
+ function sanitize(v) {
24
+ return String(v || 'deck').trim().toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'deck'
36
26
  }
37
27
 
38
- function getExportFileName({ project, selectedCustomer }) {
39
- const baseName = selectedCustomer
28
+ function buildFileName({ project, selectedCustomer }) {
29
+ const base = selectedCustomer
40
30
  ? `${selectedCustomer} ${document.title || project || 'deck'}`
41
31
  : document.title || project || 'deck'
42
-
43
- return `${sanitizeFileName(baseName)}.pdf`
32
+ return `${sanitize(base)}.pdf`
44
33
  }
45
34
 
46
- function triggerDownload(blob, fileName) {
47
- const blobUrl = URL.createObjectURL(blob)
48
- const link = document.createElement('a')
49
- link.href = blobUrl
50
- link.download = fileName
51
- document.body.append(link)
52
- link.click()
53
- link.remove()
54
- window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000)
35
+ /**
36
+ * Pause animations on a slide for deterministic capture. Returns restore fn.
37
+ */
38
+ function pauseAnimations(slide) {
39
+ const undo = []
40
+ const pause = (el) => {
41
+ const orig = el.style.animationPlayState
42
+ el.style.animationPlayState = 'paused'
43
+ undo.push(() => { el.style.animationPlayState = orig })
44
+ }
45
+ pause(slide)
46
+ slide.querySelectorAll('*').forEach(pause)
47
+ return () => { for (let i = undo.length - 1; i >= 0; i--) undo[i]() }
55
48
  }
56
49
 
57
50
  export async function exportDeckPdf({
@@ -62,73 +55,79 @@ export async function exportDeckPdf({
62
55
  totalSlides,
63
56
  onProgress,
64
57
  }) {
65
- const deckElement = getDeckElement()
66
- const slideNodes = getSlideNodes(deckElement)
58
+ const deck = document.querySelector('.deck')
59
+ const slides = Array.from(deck?.querySelectorAll('.slide') || [])
60
+ if (!deck || slides.length === 0) throw new Error('No slides found')
67
61
 
68
- if (!deckElement || slideNodes.length === 0) {
69
- throw new Error('No slides found to export')
70
- }
71
-
72
- const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
73
- import('html2canvas'),
62
+ // Dynamic imports tree-shaken, only loaded on export
63
+ const [{ domToPng }, { jsPDF }] = await Promise.all([
64
+ import('modern-screenshot'),
74
65
  import('jspdf'),
75
66
  ])
76
67
 
77
- const pageWidth = deckElement.clientWidth || DEFAULT_PAGE_WIDTH
78
- const pageHeight = deckElement.clientHeight || DEFAULT_PAGE_HEIGHT
79
- const backgroundColor = getComputedStyle(document.documentElement)
80
- .getPropertyValue('--bg-deep')
81
- .trim() || '#080b10'
68
+ const bg = getComputedStyle(document.documentElement)
69
+ .getPropertyValue('--bg-deep').trim() || '#080b10'
82
70
  const scale = Math.min(window.devicePixelRatio || 1, 2)
71
+
83
72
  const pdf = new jsPDF({
84
73
  orientation: 'landscape',
85
74
  unit: 'px',
86
- format: [DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT],
75
+ format: [PAGE_W, PAGE_H],
87
76
  compress: true,
88
77
  hotfixes: ['px_scaling'],
89
78
  })
90
79
 
91
- await waitForFonts()
80
+ if (document.fonts?.ready) await document.fonts.ready
92
81
 
93
82
  try {
94
- for (let index = 0; index < totalSlides; index += 1) {
95
- onProgress?.({ current: index + 1, total: totalSlides })
96
- goTo(index)
83
+ for (let i = 0; i < totalSlides; i++) {
84
+ onProgress?.({ current: i + 1, total: totalSlides })
85
+ goTo(i)
97
86
  await waitForPaint()
98
- await wait(EXPORT_TRANSITION_MS)
87
+ await wait(SETTLE_MS)
99
88
 
100
- const activeSlide = document.querySelector('.slide.active') || slideNodes[index]
101
- if (!activeSlide) {
102
- throw new Error(`Slide ${index + 1} could not be rendered`)
103
- }
89
+ const active = document.querySelector('.slide.active') || slides[i]
90
+ if (!active) throw new Error(`Slide ${i + 1} not found`)
91
+
92
+ const restore = pauseAnimations(active)
93
+ await waitForPaint()
104
94
 
105
- const canvas = await html2canvas(activeSlide, {
106
- backgroundColor,
107
- useCORS: true,
108
- allowTaint: true,
109
- logging: false,
110
- scale,
111
- width: pageWidth,
112
- height: pageHeight,
113
- windowWidth: window.innerWidth,
114
- windowHeight: window.innerHeight,
115
- })
116
-
117
- const imageData = canvas.toDataURL('image/png')
118
- if (index > 0) {
119
- pdf.addPage([DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT], 'landscape')
95
+ let dataUrl
96
+ try {
97
+ dataUrl = await domToPng(active, {
98
+ width: active.clientWidth || PAGE_W,
99
+ height: active.clientHeight || PAGE_H,
100
+ backgroundColor: bg,
101
+ scale,
102
+ style: {
103
+ // Ensure the captured element is visible and static
104
+ opacity: '1',
105
+ transform: 'none',
106
+ transition: 'none',
107
+ },
108
+ })
109
+ } finally {
110
+ restore()
120
111
  }
121
112
 
122
- pdf.addImage(imageData, 'PNG', 0, 0, DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT, undefined, 'FAST')
113
+ if (i > 0) pdf.addPage([PAGE_W, PAGE_H], 'landscape')
114
+ pdf.addImage(dataUrl, 'PNG', 0, 0, PAGE_W, PAGE_H, undefined, 'FAST')
123
115
  }
124
116
  } finally {
125
117
  goTo(current)
126
118
  await waitForPaint()
127
119
  }
128
120
 
121
+ // Direct download — no dialog
129
122
  const blob = pdf.output('blob')
130
- const fileName = getExportFileName({ project, selectedCustomer })
131
- triggerDownload(blob, fileName)
132
-
133
- return { fileName }
134
- }
123
+ const url = URL.createObjectURL(blob)
124
+ const a = document.createElement('a')
125
+ a.href = url
126
+ a.download = buildFileName({ project, selectedCustomer })
127
+ document.body.appendChild(a)
128
+ a.click()
129
+ a.remove()
130
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
131
+
132
+ return { fileName: a.download }
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckio/deck-engine",
3
- "version": "1.7.7",
3
+ "version": "1.7.8",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -35,8 +35,8 @@
35
35
  "**/*.css"
36
36
  ],
37
37
  "dependencies": {
38
- "html2canvas": "^1.4.1",
39
- "jspdf": "^3.0.3"
38
+ "jspdf": "^3.0.3",
39
+ "modern-screenshot": "^4.6.8"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": "^19.1.0",