@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.
- package/components/BottomBar.jsx +9 -0
- package/components/BottomBar.module.css +17 -0
- package/components/Navigation.jsx +195 -0
- package/components/Navigation.module.css +210 -0
- package/components/Slide.jsx +43 -0
- package/components/exportDeckPdf.js +142 -0
- package/components/exportDeckPptx.js +127 -0
- package/context/SlideContext.jsx +190 -0
- package/index.js +5 -0
- package/instructions/AGENTS.md +26 -0
- package/instructions/deck-config.instructions.md +34 -0
- package/instructions/deck-project.instructions.md +34 -0
- package/instructions/slide-css.instructions.md +96 -0
- package/instructions/slide-jsx.instructions.md +34 -0
- package/package.json +59 -0
- package/scripts/capture-screen.mjs +127 -0
- package/scripts/export-pdf.mjs +287 -0
- package/scripts/generate-image.mjs +110 -0
- package/scripts/init-project.mjs +214 -0
- package/skills/deck-add-slide/SKILL.md +236 -0
- package/skills/deck-delete-slide/SKILL.md +51 -0
- package/skills/deck-generate-image/SKILL.md +85 -0
- package/skills/deck-inspect/SKILL.md +60 -0
- package/skills/deck-sketch/SKILL.md +91 -0
- package/skills/deck-validate-project/SKILL.md +81 -0
- package/slides/GenericThankYouSlide.jsx +31 -0
- package/styles/global.css +392 -0
- package/themes/dark.css +151 -0
- package/themes/light.css +152 -0
- package/themes/shadcn.css +212 -0
- package/themes/theme-loader.js +47 -0
- package/vite.js +67 -0
|
@@ -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
|
+
}
|