@deckio/deck-engine 1.7.6 → 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.
@@ -0,0 +1,133 @@
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))
17
+
18
+ async function waitForPaint() {
19
+ await new Promise((r) => requestAnimationFrame(() => r()))
20
+ await new Promise((r) => requestAnimationFrame(() => r()))
21
+ }
22
+
23
+ function sanitize(v) {
24
+ return String(v || 'deck').trim().toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'deck'
26
+ }
27
+
28
+ function buildFileName({ project, selectedCustomer }) {
29
+ const base = selectedCustomer
30
+ ? `${selectedCustomer} ${document.title || project || 'deck'}`
31
+ : document.title || project || 'deck'
32
+ return `${sanitize(base)}.pdf`
33
+ }
34
+
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]() }
48
+ }
49
+
50
+ export async function exportDeckPdf({
51
+ current,
52
+ goTo,
53
+ project,
54
+ selectedCustomer,
55
+ totalSlides,
56
+ onProgress,
57
+ }) {
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')
61
+
62
+ // Dynamic imports — tree-shaken, only loaded on export
63
+ const [{ domToPng }, { jsPDF }] = await Promise.all([
64
+ import('modern-screenshot'),
65
+ import('jspdf'),
66
+ ])
67
+
68
+ const bg = getComputedStyle(document.documentElement)
69
+ .getPropertyValue('--bg-deep').trim() || '#080b10'
70
+ const scale = Math.min(window.devicePixelRatio || 1, 2)
71
+
72
+ const pdf = new jsPDF({
73
+ orientation: 'landscape',
74
+ unit: 'px',
75
+ format: [PAGE_W, PAGE_H],
76
+ compress: true,
77
+ hotfixes: ['px_scaling'],
78
+ })
79
+
80
+ if (document.fonts?.ready) await document.fonts.ready
81
+
82
+ try {
83
+ for (let i = 0; i < totalSlides; i++) {
84
+ onProgress?.({ current: i + 1, total: totalSlides })
85
+ goTo(i)
86
+ await waitForPaint()
87
+ await wait(SETTLE_MS)
88
+
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()
94
+
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()
111
+ }
112
+
113
+ if (i > 0) pdf.addPage([PAGE_W, PAGE_H], 'landscape')
114
+ pdf.addImage(dataUrl, 'PNG', 0, 0, PAGE_W, PAGE_H, undefined, 'FAST')
115
+ }
116
+ } finally {
117
+ goTo(current)
118
+ await waitForPaint()
119
+ }
120
+
121
+ // Direct download — no dialog
122
+ const blob = pdf.output('blob')
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
+ }
@@ -1,171 +1,171 @@
1
- import {
2
- createContext,
3
- useContext,
4
- useState,
5
- useCallback,
6
- useEffect,
7
- } from 'react'
8
-
9
- /* ╔══════════════════════════════════════════════════════════════╗
10
- * ║ ║
11
- * ║ ▂▃▅▇█ S L I D E C O N T E X T █▇▅▃▂ ║
12
- * ║ ║
13
- * ║ Central state for slide navigation, persistence, ║
14
- * ║ keyboard / touch input, and customer selection. ║
15
- * ║ ║
16
- * ╚══════════════════════════════════════════════════════════════╝ */
17
-
18
- const SlideContext = createContext()
19
-
20
- /* ┌─────────────────────────────────────────────────────────────┐
21
- * │ ◆ H E L P E R S │
22
- * └─────────────────────────────────────────────────────────────┘ */
23
-
24
- /**
25
- * Recover the last-viewed slide from sessionStorage.
26
- * Survives Vite HMR so you stay on the same slide during dev.
27
- *
28
- * sessionStorage key format: slide:<project>
29
- * returns 0 when nothing stored or value is out of range.
30
- */
31
- function getStoredSlide(project, totalSlides) {
32
- try {
33
- const idx = parseInt(sessionStorage.getItem(`slide:${project}`), 10)
34
- return Number.isFinite(idx) && idx >= 0 && idx < totalSlides ? idx : 0
35
- } catch {
36
- return 0
37
- }
38
- }
39
-
40
- /* ╭──────────────────────────────────────────────────────────────╮
41
- * │ ◈ P R O V I D E R │
42
- * ╰──────────────────────────────────────────────────────────────╯ */
43
-
44
- export function SlideProvider({ children, totalSlides, project, slides }) {
45
- const [current, setCurrent] = useState(() =>
46
- getStoredSlide(project, totalSlides),
47
- )
48
- const [selectedCustomer, setSelectedCustomer] = useState(null)
49
-
50
- /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
51
- * ░ Persist slide index ─ HMR keeps position ░
52
- * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */
53
-
54
- useEffect(() => {
55
- try {
56
- sessionStorage.setItem(`slide:${project}`, current)
57
- } catch {
58
- /* storage full / unavailable – ignore */
59
- }
60
- }, [current, project])
61
-
62
- /* 📡 ─────────────────────────────────────────────
63
- * │ Notify parent window of slide changes │
64
- * │ (used by deck-launcher to provide context) │
65
- * ───────────────────────────────────────── 📡 */
66
-
67
- useEffect(() => {
68
- try {
69
- if (window.parent && window.parent !== window) {
70
- const slideName = slides?.[current]?.displayName || slides?.[current]?.name || ''
71
- window.parent.postMessage({
72
- type: 'deck:slide',
73
- project,
74
- slideIndex: current,
75
- slideName,
76
- totalSlides,
77
- }, '*')
78
- }
79
- } catch {
80
- /* cross-origin or non-iframe – ignore */
81
- }
82
- }, [current, project, totalSlides, slides])
83
-
84
- /* ▸ ▸ ▸ Navigation helpers ◂ ◂ ◂ */
85
-
86
- const go = useCallback(
87
- (dir) => {
88
- setCurrent((prev) => {
89
- const next = prev + dir
90
- return next < 0 || next >= totalSlides ? prev : next
91
- })
92
- },
93
- [totalSlides],
94
- )
95
-
96
- const goTo = useCallback(
97
- (idx) => {
98
- if (idx >= 0 && idx < totalSlides) setCurrent(idx)
99
- },
100
- [totalSlides],
101
- )
102
-
103
- /* ⌨ ─────────────────────────────────────────────────────
104
- * │ Keyboard → ← Space PageDown PageUp Enter │
105
- * ───────────────────────────────────────────────── ⌨ */
106
-
107
- useEffect(() => {
108
- const handler = (e) => {
109
- if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown' || e.key === 'Enter') {
110
- e.preventDefault()
111
- go(1)
112
- }
113
- if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
114
- e.preventDefault()
115
- go(-1)
116
- }
117
- }
118
-
119
- document.addEventListener('keydown', handler)
120
- return () => document.removeEventListener('keydown', handler)
121
- }, [go])
122
-
123
- /* 👆 ─────────────────────────────────
124
- * │ Touch / swipe (threshold 50px) │
125
- * ───────────────────────────── 👆 */
126
-
127
- useEffect(() => {
128
- let touchX = 0
129
-
130
- const onStart = (e) => {
131
- touchX = e.changedTouches[0].screenX
132
- }
133
- const onEnd = (e) => {
134
- const diff = touchX - e.changedTouches[0].screenX
135
- if (Math.abs(diff) > 50) go(diff > 0 ? 1 : -1)
136
- }
137
-
138
- document.addEventListener('touchstart', onStart)
139
- document.addEventListener('touchend', onEnd)
140
- return () => {
141
- document.removeEventListener('touchstart', onStart)
142
- document.removeEventListener('touchend', onEnd)
143
- }
144
- }, [go])
145
-
146
- /* ◇─────────────── render ───────────────◇ */
147
-
148
- return (
149
- <SlideContext.Provider
150
- value={{
151
- current,
152
- totalSlides,
153
- go,
154
- goTo,
155
- selectedCustomer,
156
- setSelectedCustomer,
157
- project,
158
- }}
159
- >
160
- {children}
161
- </SlideContext.Provider>
162
- )
163
- }
164
-
165
- /* ┌─────────────────────────────────────────────────────────────┐
166
- * │ ◆ H O O K │
167
- * └─────────────────────────────────────────────────────────────┘ */
168
-
169
- export function useSlides() {
170
- return useContext(SlideContext)
171
- }
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useCallback,
6
+ useEffect,
7
+ } from 'react'
8
+
9
+ /* ╔══════════════════════════════════════════════════════════════╗
10
+ * ║ ║
11
+ * ║ ▂▃▅▇█ S L I D E C O N T E X T █▇▅▃▂ ║
12
+ * ║ ║
13
+ * ║ Central state for slide navigation, persistence, ║
14
+ * ║ keyboard / touch input, and customer selection. ║
15
+ * ║ ║
16
+ * ╚══════════════════════════════════════════════════════════════╝ */
17
+
18
+ const SlideContext = createContext()
19
+
20
+ /* ┌─────────────────────────────────────────────────────────────┐
21
+ * │ ◆ H E L P E R S │
22
+ * └─────────────────────────────────────────────────────────────┘ */
23
+
24
+ /**
25
+ * Recover the last-viewed slide from sessionStorage.
26
+ * Survives Vite HMR so you stay on the same slide during dev.
27
+ *
28
+ * sessionStorage key format: slide:<project>
29
+ * returns 0 when nothing stored or value is out of range.
30
+ */
31
+ function getStoredSlide(project, totalSlides) {
32
+ try {
33
+ const idx = parseInt(sessionStorage.getItem(`slide:${project}`), 10)
34
+ return Number.isFinite(idx) && idx >= 0 && idx < totalSlides ? idx : 0
35
+ } catch {
36
+ return 0
37
+ }
38
+ }
39
+
40
+ /* ╭──────────────────────────────────────────────────────────────╮
41
+ * │ ◈ P R O V I D E R │
42
+ * ╰──────────────────────────────────────────────────────────────╯ */
43
+
44
+ export function SlideProvider({ children, totalSlides, project, slides }) {
45
+ const [current, setCurrent] = useState(() =>
46
+ getStoredSlide(project, totalSlides),
47
+ )
48
+ const [selectedCustomer, setSelectedCustomer] = useState(null)
49
+
50
+ /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
51
+ * ░ Persist slide index ─ HMR keeps position ░
52
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */
53
+
54
+ useEffect(() => {
55
+ try {
56
+ sessionStorage.setItem(`slide:${project}`, current)
57
+ } catch {
58
+ /* storage full / unavailable – ignore */
59
+ }
60
+ }, [current, project])
61
+
62
+ /* 📡 ─────────────────────────────────────────────
63
+ * │ Notify parent window of slide changes │
64
+ * │ (used by deck-launcher to provide context) │
65
+ * ───────────────────────────────────────── 📡 */
66
+
67
+ useEffect(() => {
68
+ try {
69
+ if (window.parent && window.parent !== window) {
70
+ const slideName = slides?.[current]?.displayName || slides?.[current]?.name || ''
71
+ window.parent.postMessage({
72
+ type: 'deck:slide',
73
+ project,
74
+ slideIndex: current,
75
+ slideName,
76
+ totalSlides,
77
+ }, '*')
78
+ }
79
+ } catch {
80
+ /* cross-origin or non-iframe – ignore */
81
+ }
82
+ }, [current, project, totalSlides, slides])
83
+
84
+ /* ▸ ▸ ▸ Navigation helpers ◂ ◂ ◂ */
85
+
86
+ const go = useCallback(
87
+ (dir) => {
88
+ setCurrent((prev) => {
89
+ const next = prev + dir
90
+ return next < 0 || next >= totalSlides ? prev : next
91
+ })
92
+ },
93
+ [totalSlides],
94
+ )
95
+
96
+ const goTo = useCallback(
97
+ (idx) => {
98
+ if (idx >= 0 && idx < totalSlides) setCurrent(idx)
99
+ },
100
+ [totalSlides],
101
+ )
102
+
103
+ /* ⌨ ─────────────────────────────────────────────────────
104
+ * │ Keyboard → ← Space PageDown PageUp Enter │
105
+ * ───────────────────────────────────────────────── ⌨ */
106
+
107
+ useEffect(() => {
108
+ const handler = (e) => {
109
+ if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown' || e.key === 'Enter') {
110
+ e.preventDefault()
111
+ go(1)
112
+ }
113
+ if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
114
+ e.preventDefault()
115
+ go(-1)
116
+ }
117
+ }
118
+
119
+ document.addEventListener('keydown', handler)
120
+ return () => document.removeEventListener('keydown', handler)
121
+ }, [go])
122
+
123
+ /* 👆 ─────────────────────────────────
124
+ * │ Touch / swipe (threshold 50px) │
125
+ * ───────────────────────────── 👆 */
126
+
127
+ useEffect(() => {
128
+ let touchX = 0
129
+
130
+ const onStart = (e) => {
131
+ touchX = e.changedTouches[0].screenX
132
+ }
133
+ const onEnd = (e) => {
134
+ const diff = touchX - e.changedTouches[0].screenX
135
+ if (Math.abs(diff) > 50) go(diff > 0 ? 1 : -1)
136
+ }
137
+
138
+ document.addEventListener('touchstart', onStart)
139
+ document.addEventListener('touchend', onEnd)
140
+ return () => {
141
+ document.removeEventListener('touchstart', onStart)
142
+ document.removeEventListener('touchend', onEnd)
143
+ }
144
+ }, [go])
145
+
146
+ /* ◇─────────────── render ───────────────◇ */
147
+
148
+ return (
149
+ <SlideContext.Provider
150
+ value={{
151
+ current,
152
+ totalSlides,
153
+ go,
154
+ goTo,
155
+ selectedCustomer,
156
+ setSelectedCustomer,
157
+ project,
158
+ }}
159
+ >
160
+ {children}
161
+ </SlideContext.Provider>
162
+ )
163
+ }
164
+
165
+ /* ┌─────────────────────────────────────────────────────────────┐
166
+ * │ ◆ H O O K │
167
+ * └─────────────────────────────────────────────────────────────┘ */
168
+
169
+ export function useSlides() {
170
+ return useContext(SlideContext)
171
+ }
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { SlideProvider, useSlides } from './context/SlideContext.jsx'
2
- export { default as Slide } from './components/Slide.jsx'
3
- export { default as Navigation } from './components/Navigation.jsx'
4
- export { default as BottomBar } from './components/BottomBar.jsx'
5
- export { default as GenericThankYouSlide } from './slides/GenericThankYouSlide.jsx'
1
+ export { SlideProvider, useSlides } from './context/SlideContext.jsx'
2
+ export { default as Slide } from './components/Slide.jsx'
3
+ export { default as Navigation } from './components/Navigation.jsx'
4
+ export { default as BottomBar } from './components/BottomBar.jsx'
5
+ export { default as GenericThankYouSlide } from './slides/GenericThankYouSlide.jsx'
@@ -1,26 +1,26 @@
1
- # Deck Project
2
-
3
- This is a presentation deck built with `@deckio/deck-engine`.
4
-
5
- ## Purpose
6
-
7
- Create and maintain slide-based presentations. Each project is a self-contained deck with its own theme, data, and slides.
8
-
9
- ## What to do
10
-
11
- - Create, edit, and delete slides in `src/slides/`
12
- - Manage project data in `src/data/`
13
- - Register and reorder slides in `deck.config.js`
14
-
15
- ## What NOT to do
16
-
17
- - Do not modify `App.jsx`, `main.jsx`, `vite.config.js`, `package.json`, or `index.html` — these are scaffolding files driven by the engine
18
- - Do not modify anything in `node_modules/` or the engine itself
19
- - Do not add dependencies without being asked
20
-
21
- ## Stack
22
-
23
- - React 19, Vite, CSS Modules
24
- - `@deckio/deck-engine` provides: `Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
25
- - See `.github/instructions/` for detailed conventions on slide JSX, CSS modules, and deck.config.js
26
- - See `.github/skills/` for step-by-step workflows (e.g., adding a slide)
1
+ # Deck Project
2
+
3
+ This is a presentation deck built with `@deckio/deck-engine`.
4
+
5
+ ## Purpose
6
+
7
+ Create and maintain slide-based presentations. Each project is a self-contained deck with its own theme, data, and slides.
8
+
9
+ ## What to do
10
+
11
+ - Create, edit, and delete slides in `src/slides/`
12
+ - Manage project data in `src/data/`
13
+ - Register and reorder slides in `deck.config.js`
14
+
15
+ ## What NOT to do
16
+
17
+ - Do not modify `App.jsx`, `main.jsx`, `vite.config.js`, `package.json`, or `index.html` — these are scaffolding files driven by the engine
18
+ - Do not modify anything in `node_modules/` or the engine itself
19
+ - Do not add dependencies without being asked
20
+
21
+ ## Stack
22
+
23
+ - React 19, Vite, CSS Modules
24
+ - `@deckio/deck-engine` provides: `Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
25
+ - See `.github/instructions/` for detailed conventions on slide JSX, CSS modules, and deck.config.js
26
+ - See `.github/skills/` for step-by-step workflows (e.g., adding a slide)
@@ -1,34 +1,34 @@
1
- ---
2
- description: "Use when editing deck.config.js to register slides or modify project configuration."
3
- applyTo: "**/deck.config.js"
4
- ---
5
-
6
- # deck.config.js Conventions
7
-
8
- ## Structure
9
-
10
- ```js
11
- import CoverSlide from './src/slides/CoverSlide.jsx'
12
- import MySlide from './src/slides/MySlide.jsx'
13
-
14
- export default {
15
- id: 'my-project', // lowercase-hyphens, unique slug
16
- title: 'Project Title',
17
- subtitle: 'Tagline',
18
- description: 'Metadata',
19
- icon: '🚀', // emoji for launcher
20
- accent: '#7c3aed', // CSS color, sets --accent
21
- order: 1, // sort position in launcher
22
- slides: [
23
- CoverSlide,
24
- MySlide,
25
- // ThankYouSlide is typically last
26
- ],
27
- }
28
- ```
29
-
30
- ## Registering a new slide
31
-
32
- 1. Add an import at the top: `import NewSlide from './src/slides/NewSlide.jsx'`
33
- 2. Insert the component in the `slides` array at the desired position
34
- 3. Indices are assigned by array position — do not manage them manually
1
+ ---
2
+ description: "Use when editing deck.config.js to register slides or modify project configuration."
3
+ applyTo: "**/deck.config.js"
4
+ ---
5
+
6
+ # deck.config.js Conventions
7
+
8
+ ## Structure
9
+
10
+ ```js
11
+ import CoverSlide from './src/slides/CoverSlide.jsx'
12
+ import MySlide from './src/slides/MySlide.jsx'
13
+
14
+ export default {
15
+ id: 'my-project', // lowercase-hyphens, unique slug
16
+ title: 'Project Title',
17
+ subtitle: 'Tagline',
18
+ description: 'Metadata',
19
+ icon: '🚀', // emoji for launcher
20
+ accent: '#7c3aed', // CSS color, sets --accent
21
+ order: 1, // sort position in launcher
22
+ slides: [
23
+ CoverSlide,
24
+ MySlide,
25
+ // ThankYouSlide is typically last
26
+ ],
27
+ }
28
+ ```
29
+
30
+ ## Registering a new slide
31
+
32
+ 1. Add an import at the top: `import NewSlide from './src/slides/NewSlide.jsx'`
33
+ 2. Insert the component in the `slides` array at the desired position
34
+ 3. Indices are assigned by array position — do not manage them manually