@deckio/deck-engine 0.1.0

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,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('--background').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
+ }
@@ -0,0 +1,190 @@
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
+ const DEFAULT_THEME = 'dark'
45
+
46
+ export function SlideProvider({ children, totalSlides, project, slides, theme }) {
47
+ const [current, setCurrent] = useState(() =>
48
+ getStoredSlide(project, totalSlides),
49
+ )
50
+ const [selectedCustomer, setSelectedCustomer] = useState(null)
51
+ const [activeTheme, setActiveTheme] = useState(theme || DEFAULT_THEME)
52
+
53
+ /* 🎨 ─────────────────────────────────────────────
54
+ * │ Theme → data-theme on <html> for CSS hooks │
55
+ * ───────────────────────────────────────── 🎨 */
56
+
57
+ useEffect(() => {
58
+ document.documentElement.setAttribute('data-theme', activeTheme)
59
+ return () => document.documentElement.removeAttribute('data-theme')
60
+ }, [activeTheme])
61
+
62
+ // Sync if the theme prop changes at runtime (e.g. HMR / config reload)
63
+ useEffect(() => {
64
+ if (theme && theme !== activeTheme) setActiveTheme(theme)
65
+ }, [theme])
66
+
67
+ /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
68
+ * ░ Persist slide index ─ HMR keeps position ░
69
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */
70
+
71
+ useEffect(() => {
72
+ try {
73
+ sessionStorage.setItem(`slide:${project}`, current)
74
+ } catch {
75
+ /* storage full / unavailable – ignore */
76
+ }
77
+ }, [current, project])
78
+
79
+ /* 📡 ─────────────────────────────────────────────
80
+ * │ Notify parent window of slide changes │
81
+ * │ (used by deck-launcher to provide context) │
82
+ * ───────────────────────────────────────── 📡 */
83
+
84
+ useEffect(() => {
85
+ try {
86
+ if (window.parent && window.parent !== window) {
87
+ const slideName = slides?.[current]?.displayName || slides?.[current]?.name || ''
88
+ window.parent.postMessage({
89
+ type: 'deck:slide',
90
+ project,
91
+ slideIndex: current,
92
+ slideName,
93
+ totalSlides,
94
+ }, '*')
95
+ }
96
+ } catch {
97
+ /* cross-origin or non-iframe – ignore */
98
+ }
99
+ }, [current, project, totalSlides, slides])
100
+
101
+ /* ▸ ▸ ▸ Navigation helpers ◂ ◂ ◂ */
102
+
103
+ const go = useCallback(
104
+ (dir) => {
105
+ setCurrent((prev) => {
106
+ const next = prev + dir
107
+ return next < 0 || next >= totalSlides ? prev : next
108
+ })
109
+ },
110
+ [totalSlides],
111
+ )
112
+
113
+ const goTo = useCallback(
114
+ (idx) => {
115
+ if (idx >= 0 && idx < totalSlides) setCurrent(idx)
116
+ },
117
+ [totalSlides],
118
+ )
119
+
120
+ /* ⌨ ─────────────────────────────────────────────────────
121
+ * │ Keyboard → ← Space PageDown PageUp Enter │
122
+ * ───────────────────────────────────────────────── ⌨ */
123
+
124
+ useEffect(() => {
125
+ const handler = (e) => {
126
+ if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown' || e.key === 'Enter') {
127
+ e.preventDefault()
128
+ go(1)
129
+ }
130
+ if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
131
+ e.preventDefault()
132
+ go(-1)
133
+ }
134
+ }
135
+
136
+ document.addEventListener('keydown', handler)
137
+ return () => document.removeEventListener('keydown', handler)
138
+ }, [go])
139
+
140
+ /* 👆 ─────────────────────────────────
141
+ * │ Touch / swipe (threshold 50px) │
142
+ * ───────────────────────────── 👆 */
143
+
144
+ useEffect(() => {
145
+ let touchX = 0
146
+
147
+ const onStart = (e) => {
148
+ touchX = e.changedTouches[0].screenX
149
+ }
150
+ const onEnd = (e) => {
151
+ const diff = touchX - e.changedTouches[0].screenX
152
+ if (Math.abs(diff) > 50) go(diff > 0 ? 1 : -1)
153
+ }
154
+
155
+ document.addEventListener('touchstart', onStart)
156
+ document.addEventListener('touchend', onEnd)
157
+ return () => {
158
+ document.removeEventListener('touchstart', onStart)
159
+ document.removeEventListener('touchend', onEnd)
160
+ }
161
+ }, [go])
162
+
163
+ /* ◇─────────────── render ───────────────◇ */
164
+
165
+ return (
166
+ <SlideContext.Provider
167
+ value={{
168
+ current,
169
+ totalSlides,
170
+ go,
171
+ goTo,
172
+ selectedCustomer,
173
+ setSelectedCustomer,
174
+ project,
175
+ theme: activeTheme,
176
+ setTheme: setActiveTheme,
177
+ }}
178
+ >
179
+ {children}
180
+ </SlideContext.Provider>
181
+ )
182
+ }
183
+
184
+ /* ┌─────────────────────────────────────────────────────────────┐
185
+ * │ ◆ H O O K │
186
+ * └─────────────────────────────────────────────────────────────┘ */
187
+
188
+ export function useSlides() {
189
+ return useContext(SlideContext)
190
+ }
package/index.js ADDED
@@ -0,0 +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'
@@ -0,0 +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)
@@ -0,0 +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
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: "Use when working in any deck-project folder. Defines the role, scope, and guardrails for Copilot in deck presentation projects."
3
+ applyTo: "deck-project-*/**"
4
+ ---
5
+
6
+ # Deck Project Scope
7
+
8
+ You are a **slide builder** for a presentation deck built with `@deckio/deck-engine`.
9
+
10
+ ## Your role
11
+
12
+ - Create new slide components (JSX + CSS module pairs)
13
+ - Edit existing slides
14
+ - Manage data files in `src/data/`
15
+ - Register / reorder slides in `deck.config.js`
16
+
17
+ ## Out of scope — do NOT modify
18
+
19
+ - `App.jsx`, `main.jsx` — these are generic, engine-driven, identical across projects
20
+ - `vite.config.js`, `package.json`, `index.html` — project scaffolding, don't touch
21
+ - Anything in `node_modules/` or the deck-engine package itself
22
+
23
+ ## Project architecture
24
+
25
+ - `deck.config.js` — single source of truth: metadata + slide array
26
+ - `src/slides/` — one `PascalCase.jsx` + matching `.module.css` per slide
27
+ - `src/data/` — ESM exports for logos, speakers, opportunity data, governance
28
+ - The engine (`@deckio/deck-engine`) provides: `Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
29
+
30
+ ## Data conventions
31
+
32
+ - Always use ESM imports for images: `import logo from '../data/logos/acme.png'`
33
+ - Export data as named exports or default objects
34
+ - Keep data files focused — one concern per file
@@ -0,0 +1,96 @@
1
+ ---
2
+ description: "Use when creating or editing slide CSS modules in a deck project. Enforces required properties, orb positioning, and theme variables."
3
+ applyTo: "**/slides/**/*.module.css"
4
+ ---
5
+
6
+ # Slide CSS Module Conventions
7
+
8
+ ## Required root class
9
+
10
+ ```css
11
+ .mySlide {
12
+ background: var(--bg-deep);
13
+ padding: 0 0 44px 0; /* reserve BottomBar height */
14
+ }
15
+ ```
16
+
17
+ The engine's `.slide` class already sets `flex-direction: column`, `justify-content: center`, and `overflow: hidden`. The engine also sets `flex-grow: 0` on all direct slide children, so **content stays at its natural height and is vertically centered by default** — building from the center outward. No scrolling is allowed.
18
+
19
+ For dense slides that need top-alignment, override with `justify-content: flex-start`.
20
+
21
+ ## Orb positioning recipe
22
+
23
+ ```css
24
+ .orb1 {
25
+ width: 420px; height: 420px;
26
+ top: -100px; right: -60px;
27
+ background: radial-gradient(circle at 40% 40%, var(--accent), var(--blue-glow) 50%, transparent 70%);
28
+ }
29
+ .orb2 {
30
+ width: 320px; height: 320px;
31
+ bottom: -40px; right: 100px;
32
+ background: radial-gradient(circle at 50% 50%, var(--purple-deep), rgba(110,64,201,0.25) 60%, transparent 75%);
33
+ }
34
+ ```
35
+
36
+ ## Body wrapper
37
+
38
+ ```css
39
+ .body {
40
+ position: relative;
41
+ z-index: 10;
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 24px;
45
+ }
46
+ ```
47
+
48
+ > **Do NOT add `flex: 1` or `flex-grow: 1`** to the body wrapper or any direct slide child — it stretches the wrapper to fill the slide and defeats the engine's built-in vertical centering. The engine sets `flex-grow: 0` on all direct slide children to ensure content builds from the center outward. Inner elements within the body wrapper should also avoid `flex: 1` unless they genuinely need to fill remaining space within the body.
49
+
50
+ ## Theme variables (always use these, never hard-code colors)
51
+
52
+ | Variable | Value |
53
+ |----------|-------|
54
+ | `--bg-deep` | `#080b10` |
55
+ | `--surface` | `#161b22` |
56
+ | `--border` | `#30363d` |
57
+ | `--text` | `#e6edf3` |
58
+ | `--text-muted` | `#8b949e` |
59
+ | `--accent` | project-specific |
60
+ | `--blue-glow` | `#1f6feb` |
61
+ | `--purple` | `#bc8cff` |
62
+ | `--purple-deep` | `#6e40c9` |
63
+ | `--pink` | `#f778ba` |
64
+ | `--cyan` | `#56d4dd` |
65
+ | `--green` | `#3fb950` |
66
+ | `--orange` | `#d29922` |
67
+
68
+ ## Global classes (no import needed)
69
+
70
+ `accent-bar`, `orb`, `grid-dots`, `content-frame`, `content-gutter`
71
+
72
+ ## Card pattern
73
+
74
+ ```css
75
+ .card {
76
+ background: var(--surface);
77
+ border: 1px solid var(--border);
78
+ border-radius: 16px;
79
+ padding: 24px;
80
+ }
81
+ ```
82
+
83
+ ## Typography
84
+
85
+ | Element | Size | Weight | Spacing |
86
+ |---------|------|--------|---------|
87
+ | h1 | `clamp(42px, 5vw, 72px)` | 900 | `-2px` |
88
+ | h2 | `clamp(28px, 3.2vw, 36px)` | 700 | `-0.8px` |
89
+ | h3 | `16px–20px` | 700 | `-0.3px` |
90
+ | Subtitle | `17px` | 300–400 | — |
91
+ | Body | `13px–14px` | 400 | — |
92
+ | Badge | `10px–11px` | 600–700 | `1.5px` |
93
+
94
+ ## Content density limits
95
+
96
+ Slides must never overflow the viewport. The engine shows a **red dashed border warning** in dev mode when content exceeds the slide bounds. When content doesn't fit, split across multiple slides rather than cramming. A presentation with more slides is better than one with clipped content.
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: "Use when creating, editing, or reviewing slide JSX components in a deck project. Enforces the mandatory slide skeleton, imports, and anti-patterns."
3
+ applyTo: "**/slides/**/*.jsx"
4
+ ---
5
+
6
+ # Slide JSX Conventions
7
+
8
+ ## Imports
9
+
10
+ ```jsx
11
+ import { BottomBar, Slide } from '@deckio/deck-engine'
12
+ import styles from './MySlide.module.css'
13
+ ```
14
+
15
+ ## Mandatory skeleton (in order inside `<Slide>`)
16
+
17
+ 1. `<div className="accent-bar" />` — always first
18
+ 2. 2–4 orbs: `<div className={\`orb ${styles.orbN}\`} />`
19
+ 3. Content wrapper: `<div className={\`${styles.body} content-frame content-gutter\`}>` — all visible content here
20
+ 4. `<BottomBar text="..." />` — always last child
21
+
22
+ ## Props
23
+
24
+ Every slide receives `{ index, project, title, subtitle }`. Pass `index` to `<Slide>`.
25
+
26
+ ## Available engine exports
27
+
28
+ `Slide`, `BottomBar`, `Navigation`, `SlideProvider`, `useSlides`, `GenericThankYouSlide`
29
+
30
+ ## Anti-patterns
31
+
32
+ - Never omit `accent-bar`, `content-frame content-gutter`, or `BottomBar`
33
+ - Never use string paths for images — always `import logo from '../data/...'`
34
+ - Never hardcode slide indices — use `useSlides().goTo()` for navigation
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@deckio/deck-engine",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "registry": "https://registry.npmjs.org",
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/deckio-art/deck-engine.git",
12
+ "directory": "packages/deck-engine"
13
+ },
14
+ "main": "./index.js",
15
+ "module": "./index.js",
16
+ "exports": {
17
+ ".": "./index.js",
18
+ "./slides/*": "./slides/*.jsx",
19
+ "./components/*": "./components/*.jsx",
20
+ "./styles/*": "./styles/*",
21
+ "./themes/*": "./themes/*",
22
+ "./themes/theme-loader": "./themes/theme-loader.js",
23
+ "./vite": "./vite.js",
24
+ "./vite.js": "./vite.js"
25
+ },
26
+ "files": [
27
+ "index.js",
28
+ "vite.js",
29
+ "components",
30
+ "context",
31
+ "slides",
32
+ "styles",
33
+ "themes",
34
+ "scripts",
35
+ "skills",
36
+ "instructions"
37
+ ],
38
+ "sideEffects": [
39
+ "**/*.css"
40
+ ],
41
+ "scripts": {
42
+ "prepublishOnly": "npm test --if-present"
43
+ },
44
+ "dependencies": {
45
+ "@tailwindcss/vite": "^4.2.1",
46
+ "jspdf": "^3.0.3",
47
+ "modern-screenshot": "^4.6.8",
48
+ "pptxgenjs": "^3.12.0",
49
+ "tailwindcss": "^4.2.1"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^19.1.0",
53
+ "react-dom": "^19.1.0"
54
+ },
55
+ "optionalDependencies": {
56
+ "puppeteer": "^24.38.0",
57
+ "puppeteer-core": "^24.38.0"
58
+ }
59
+ }