@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.
@@ -0,0 +1,134 @@
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
+ }
10
+
11
+ 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')
32
+ }
33
+
34
+ function getSlideNodes(deckElement) {
35
+ return Array.from(deckElement?.querySelectorAll('.slide') || [])
36
+ }
37
+
38
+ function getExportFileName({ project, selectedCustomer }) {
39
+ const baseName = selectedCustomer
40
+ ? `${selectedCustomer} ${document.title || project || 'deck'}`
41
+ : document.title || project || 'deck'
42
+
43
+ return `${sanitizeFileName(baseName)}.pdf`
44
+ }
45
+
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)
55
+ }
56
+
57
+ export async function exportDeckPdf({
58
+ current,
59
+ goTo,
60
+ project,
61
+ selectedCustomer,
62
+ totalSlides,
63
+ onProgress,
64
+ }) {
65
+ const deckElement = getDeckElement()
66
+ const slideNodes = getSlideNodes(deckElement)
67
+
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'),
74
+ import('jspdf'),
75
+ ])
76
+
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'
82
+ const scale = Math.min(window.devicePixelRatio || 1, 2)
83
+ const pdf = new jsPDF({
84
+ orientation: 'landscape',
85
+ unit: 'px',
86
+ format: [DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT],
87
+ compress: true,
88
+ hotfixes: ['px_scaling'],
89
+ })
90
+
91
+ await waitForFonts()
92
+
93
+ try {
94
+ for (let index = 0; index < totalSlides; index += 1) {
95
+ onProgress?.({ current: index + 1, total: totalSlides })
96
+ goTo(index)
97
+ await waitForPaint()
98
+ await wait(EXPORT_TRANSITION_MS)
99
+
100
+ const activeSlide = document.querySelector('.slide.active') || slideNodes[index]
101
+ if (!activeSlide) {
102
+ throw new Error(`Slide ${index + 1} could not be rendered`)
103
+ }
104
+
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')
120
+ }
121
+
122
+ pdf.addImage(imageData, 'PNG', 0, 0, DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT, undefined, 'FAST')
123
+ }
124
+ } finally {
125
+ goTo(current)
126
+ await waitForPaint()
127
+ }
128
+
129
+ const blob = pdf.output('blob')
130
+ const fileName = getExportFileName({ project, selectedCustomer })
131
+ triggerDownload(blob, fileName)
132
+
133
+ return { fileName }
134
+ }
@@ -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