@deckio/deck-engine 1.7.4
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 +106 -0
- package/components/Navigation.module.css +145 -0
- package/components/Slide.jsx +15 -0
- package/context/SlideContext.jsx +171 -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 +91 -0
- package/instructions/slide-jsx.instructions.md +34 -0
- package/package.json +45 -0
- package/scripts/capture-screen.mjs +110 -0
- package/scripts/export-pdf.mjs +287 -0
- package/scripts/generate-image.mjs +110 -0
- package/scripts/init-project.mjs +188 -0
- package/skills/deck-add-slide/SKILL.md +217 -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 +80 -0
- package/slides/GenericThankYouSlide.jsx +31 -0
- package/slides/ThankYouSlide.module.css +131 -0
- package/styles/global.css +191 -0
- package/vite.js +26 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.bar {
|
|
2
|
+
position: absolute;
|
|
3
|
+
bottom: 0; left: 0; right: 0;
|
|
4
|
+
height: 44px;
|
|
5
|
+
background: rgba(13,17,23,0.85);
|
|
6
|
+
backdrop-filter: blur(12px);
|
|
7
|
+
border-top: 1px solid var(--border);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
padding: 0 40px;
|
|
12
|
+
font-size: 11px;
|
|
13
|
+
letter-spacing: 2.5px;
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
color: var(--text-muted);
|
|
16
|
+
z-index: 100;
|
|
17
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useSlides } from '../context/SlideContext'
|
|
2
|
+
import styles from './Navigation.module.css'
|
|
3
|
+
import { useState, useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
function resolveProp(value, context) {
|
|
6
|
+
return typeof value === 'function' ? value(context) : value
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Navigation({ pdfPath = null, pdfLabel = 'Deck PDF' }) {
|
|
10
|
+
const { current, totalSlides, go, goTo, selectedCustomer, project } = useSlides()
|
|
11
|
+
const [hintVisible, setHintVisible] = useState(true)
|
|
12
|
+
const [idle, setIdle] = useState(false)
|
|
13
|
+
const timerRef = useRef(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const t = setTimeout(() => setHintVisible(false), 5000)
|
|
17
|
+
return () => clearTimeout(t)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const resetIdle = () => {
|
|
22
|
+
setIdle(false)
|
|
23
|
+
clearTimeout(timerRef.current)
|
|
24
|
+
timerRef.current = setTimeout(() => setIdle(true), 2000)
|
|
25
|
+
}
|
|
26
|
+
resetIdle()
|
|
27
|
+
window.addEventListener('mousemove', resetIdle)
|
|
28
|
+
window.addEventListener('mousedown', resetIdle)
|
|
29
|
+
return () => {
|
|
30
|
+
window.removeEventListener('mousemove', resetIdle)
|
|
31
|
+
window.removeEventListener('mousedown', resetIdle)
|
|
32
|
+
clearTimeout(timerRef.current)
|
|
33
|
+
}
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
const progress = ((current + 1) / totalSlides) * 100
|
|
37
|
+
const navigationState = { current, totalSlides, selectedCustomer, project }
|
|
38
|
+
const resolvedPdfPath = resolveProp(pdfPath, navigationState)
|
|
39
|
+
const resolvedPdfLabel = resolveProp(pdfLabel, navigationState) || 'Deck PDF'
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={`${styles.navWrapper} ${idle ? styles.navHidden : ''}`}>
|
|
43
|
+
<div className={styles.progressTrack}>
|
|
44
|
+
<div className={styles.progressFill} style={{ width: `${progress}%` }} />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{current !== 0 && (
|
|
48
|
+
<button
|
|
49
|
+
className={styles.homeBtn}
|
|
50
|
+
onClick={() => goTo(0)}
|
|
51
|
+
title="Back to home"
|
|
52
|
+
>
|
|
53
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
54
|
+
<path d="M3 12l9-9 9 9" />
|
|
55
|
+
<path d="M9 21V9h6v12" />
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{resolvedPdfPath && (
|
|
61
|
+
<a
|
|
62
|
+
className={styles.exportBtn}
|
|
63
|
+
href={resolvedPdfPath}
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
title={resolvedPdfLabel}
|
|
67
|
+
>
|
|
68
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
69
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
70
|
+
<polyline points="14 2 14 8 20 8" />
|
|
71
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
72
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
73
|
+
<polyline points="10 9 9 9 8 9" />
|
|
74
|
+
</svg>
|
|
75
|
+
<span className={styles.exportLabel}>PDF</span>
|
|
76
|
+
</a>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<button
|
|
80
|
+
className={`${styles.navBtn} ${styles.prev}`}
|
|
81
|
+
disabled={current === 0}
|
|
82
|
+
onClick={() => go(-1)}
|
|
83
|
+
>
|
|
84
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
85
|
+
<polyline points="15 18 9 12 15 6" />
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
className={`${styles.navBtn} ${styles.next}`}
|
|
90
|
+
disabled={current === totalSlides - 1}
|
|
91
|
+
onClick={() => go(1)}
|
|
92
|
+
>
|
|
93
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
94
|
+
<polyline points="9 6 15 12 9 18" />
|
|
95
|
+
</svg>
|
|
96
|
+
</button>
|
|
97
|
+
|
|
98
|
+
{hintVisible && (
|
|
99
|
+
<div className={styles.keyHint}>
|
|
100
|
+
<span className={styles.kbd}>←</span>
|
|
101
|
+
<span className={styles.kbd}>→</span> or click arrows to navigate
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
.navWrapper {
|
|
2
|
+
transition: opacity 0.4s ease;
|
|
3
|
+
opacity: 1;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.navHidden {
|
|
7
|
+
opacity: 0;
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.progressTrack {
|
|
12
|
+
position: fixed;
|
|
13
|
+
top: 0; left: 0; right: 0;
|
|
14
|
+
height: 3px;
|
|
15
|
+
background: rgba(48,54,61,0.6);
|
|
16
|
+
z-index: 300;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.progressFill {
|
|
20
|
+
height: 100%;
|
|
21
|
+
background: linear-gradient(90deg, var(--purple), var(--accent), var(--cyan));
|
|
22
|
+
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
23
|
+
border-radius: 0 2px 2px 0;
|
|
24
|
+
box-shadow: 0 0 12px var(--accent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.homeBtn {
|
|
28
|
+
position: fixed;
|
|
29
|
+
top: 16px;
|
|
30
|
+
left: 20px;
|
|
31
|
+
z-index: 200;
|
|
32
|
+
width: 36px; height: 36px;
|
|
33
|
+
border-radius: 10px;
|
|
34
|
+
border: 1px solid var(--border);
|
|
35
|
+
background: rgba(22,27,34,0.8);
|
|
36
|
+
backdrop-filter: blur(8px);
|
|
37
|
+
color: var(--text);
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
transition: all 0.25s ease;
|
|
43
|
+
opacity: 0.45;
|
|
44
|
+
}
|
|
45
|
+
.homeBtn:hover {
|
|
46
|
+
opacity: 1;
|
|
47
|
+
border-color: var(--accent);
|
|
48
|
+
background: rgba(31,111,235,0.2);
|
|
49
|
+
}
|
|
50
|
+
.homeBtn svg {
|
|
51
|
+
width: 16px; height: 16px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.exportBtn {
|
|
55
|
+
position: fixed;
|
|
56
|
+
top: 16px;
|
|
57
|
+
right: 20px;
|
|
58
|
+
z-index: 200;
|
|
59
|
+
height: 36px;
|
|
60
|
+
min-width: 36px;
|
|
61
|
+
padding: 0 12px;
|
|
62
|
+
border-radius: 10px;
|
|
63
|
+
border: 1px solid var(--border);
|
|
64
|
+
background: rgba(22,27,34,0.8);
|
|
65
|
+
backdrop-filter: blur(8px);
|
|
66
|
+
color: var(--text);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
gap: 6px;
|
|
72
|
+
transition: all 0.25s ease;
|
|
73
|
+
opacity: 0.45;
|
|
74
|
+
text-decoration: none;
|
|
75
|
+
font-family: inherit;
|
|
76
|
+
}
|
|
77
|
+
.exportBtn:hover {
|
|
78
|
+
opacity: 1;
|
|
79
|
+
border-color: var(--accent);
|
|
80
|
+
background: rgba(31,111,235,0.2);
|
|
81
|
+
}
|
|
82
|
+
.exportBtn svg {
|
|
83
|
+
width: 16px; height: 16px;
|
|
84
|
+
}
|
|
85
|
+
.exportLabel {
|
|
86
|
+
font-size: 11px;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
letter-spacing: 0.04em;
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.navBtn {
|
|
93
|
+
position: fixed;
|
|
94
|
+
top: 50%;
|
|
95
|
+
transform: translateY(-50%);
|
|
96
|
+
z-index: 200;
|
|
97
|
+
width: 48px; height: 48px;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
border: 1px solid var(--border);
|
|
100
|
+
background: rgba(22,27,34,0.8);
|
|
101
|
+
backdrop-filter: blur(8px);
|
|
102
|
+
color: var(--text);
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
transition: all 0.25s ease;
|
|
108
|
+
opacity: 0.5;
|
|
109
|
+
}
|
|
110
|
+
.navBtn:hover { opacity: 1; border-color: var(--accent); background: rgba(31,111,235,0.2); }
|
|
111
|
+
.prev { left: 20px; }
|
|
112
|
+
.next { right: 20px; }
|
|
113
|
+
.navBtn svg { width: 20px; height: 20px; }
|
|
114
|
+
.navBtn:disabled { opacity: 0.15; cursor: default; pointer-events: none; }
|
|
115
|
+
|
|
116
|
+
.keyHint {
|
|
117
|
+
position: fixed;
|
|
118
|
+
bottom: 56px; right: 40px;
|
|
119
|
+
z-index: 200;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 6px;
|
|
123
|
+
font-size: 11px;
|
|
124
|
+
color: var(--text-muted);
|
|
125
|
+
opacity: 0.5;
|
|
126
|
+
animation: fadeOut 3s 5s forwards;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@keyframes fadeOut {
|
|
130
|
+
to { opacity: 0; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.kbd {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
min-width: 22px; height: 22px;
|
|
138
|
+
padding: 0 5px;
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
background: var(--surface);
|
|
142
|
+
font-size: 10px;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
font-family: inherit;
|
|
145
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useSlides } from '../context/SlideContext'
|
|
2
|
+
|
|
3
|
+
export default function Slide({ index, className = '', children }) {
|
|
4
|
+
const { current } = useSlides()
|
|
5
|
+
|
|
6
|
+
let stateClass = ''
|
|
7
|
+
if (index === current) stateClass = 'active'
|
|
8
|
+
else if (index < current) stateClass = 'exit-left'
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className={`slide ${stateClass} ${className}`} data-slide={index}>
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
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,91 @@
|
|
|
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
|
+
flex-direction: column;
|
|
14
|
+
padding: 0 0 44px 0; /* reserve BottomBar height */
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add `justify-content: center` for cover or thank-you slides.
|
|
19
|
+
|
|
20
|
+
## Orb positioning recipe
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
.orb1 {
|
|
24
|
+
width: 420px; height: 420px;
|
|
25
|
+
top: -100px; right: -60px;
|
|
26
|
+
background: radial-gradient(circle at 40% 40%, var(--accent), var(--blue-glow) 50%, transparent 70%);
|
|
27
|
+
}
|
|
28
|
+
.orb2 {
|
|
29
|
+
width: 320px; height: 320px;
|
|
30
|
+
bottom: -40px; right: 100px;
|
|
31
|
+
background: radial-gradient(circle at 50% 50%, var(--purple-deep), rgba(110,64,201,0.25) 60%, transparent 75%);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Body wrapper
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
.body {
|
|
39
|
+
position: relative;
|
|
40
|
+
z-index: 10;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
flex: 1;
|
|
45
|
+
min-height: 0;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Theme variables (always use these, never hard-code colors)
|
|
50
|
+
|
|
51
|
+
| Variable | Value |
|
|
52
|
+
|----------|-------|
|
|
53
|
+
| `--bg-deep` | `#080b10` |
|
|
54
|
+
| `--surface` | `#161b22` |
|
|
55
|
+
| `--border` | `#30363d` |
|
|
56
|
+
| `--text` | `#e6edf3` |
|
|
57
|
+
| `--text-muted` | `#8b949e` |
|
|
58
|
+
| `--accent` | project-specific |
|
|
59
|
+
| `--blue-glow` | `#1f6feb` |
|
|
60
|
+
| `--purple` | `#bc8cff` |
|
|
61
|
+
| `--purple-deep` | `#6e40c9` |
|
|
62
|
+
| `--pink` | `#f778ba` |
|
|
63
|
+
| `--cyan` | `#56d4dd` |
|
|
64
|
+
| `--green` | `#3fb950` |
|
|
65
|
+
| `--orange` | `#d29922` |
|
|
66
|
+
|
|
67
|
+
## Global classes (no import needed)
|
|
68
|
+
|
|
69
|
+
`accent-bar`, `orb`, `grid-dots`, `content-frame`, `content-gutter`
|
|
70
|
+
|
|
71
|
+
## Card pattern
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
.card {
|
|
75
|
+
background: var(--surface);
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
border-radius: 16px;
|
|
78
|
+
padding: 24px;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Typography
|
|
83
|
+
|
|
84
|
+
| Element | Size | Weight | Spacing |
|
|
85
|
+
|---------|------|--------|---------|
|
|
86
|
+
| h1 | `clamp(42px, 5vw, 72px)` | 900 | `-2px` |
|
|
87
|
+
| h2 | `clamp(28px, 3.2vw, 36px)` | 700 | `-0.8px` |
|
|
88
|
+
| h3 | `16px–20px` | 700 | `-0.3px` |
|
|
89
|
+
| Subtitle | `17px` | 300–400 | — |
|
|
90
|
+
| Body | `13px–14px` | 400 | — |
|
|
91
|
+
| Badge | `10px–11px` | 600–700 | `1.5px` |
|
|
@@ -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
|