promptslide 0.2.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/dist/index.d.ts +377 -0
- package/dist/index.js +963 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/src/commands/build.mjs +73 -0
- package/src/commands/create.mjs +197 -0
- package/src/commands/preview.mjs +22 -0
- package/src/commands/studio.mjs +27 -0
- package/src/core/animated.tsx +153 -0
- package/src/core/animation-config.ts +98 -0
- package/src/core/animation-context.tsx +54 -0
- package/src/core/index.ts +73 -0
- package/src/core/layouts/shared-footer.tsx +43 -0
- package/src/core/morph.tsx +153 -0
- package/src/core/slide-deck.tsx +430 -0
- package/src/core/slide-error-boundary.tsx +50 -0
- package/src/core/theme-context.tsx +48 -0
- package/src/core/transitions.ts +200 -0
- package/src/core/types.ts +136 -0
- package/src/core/use-slide-navigation.ts +142 -0
- package/src/core/utils.ts +8 -0
- package/src/index.mjs +70 -0
- package/src/utils/ansi.mjs +5 -0
- package/src/utils/colors.mjs +44 -0
- package/src/utils/prompts.mjs +50 -0
- package/src/utils/tsconfig.mjs +35 -0
- package/src/vite/config.mjs +40 -0
- package/src/vite/plugin.mjs +66 -0
- package/templates/default/AGENTS.md +453 -0
- package/templates/default/README.md +35 -0
- package/templates/default/package.json +26 -0
- package/templates/default/public/logo.svg +7 -0
- package/templates/default/src/App.tsx +11 -0
- package/templates/default/src/deck-config.ts +8 -0
- package/templates/default/src/globals.css +157 -0
- package/templates/default/src/layouts/slide-layout-centered.tsx +59 -0
- package/templates/default/src/slides/slide-example.tsx +53 -0
- package/templates/default/src/slides/slide-title.tsx +27 -0
- package/templates/default/src/theme.ts +8 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { AnimatePresence, LayoutGroup, motion } from "framer-motion"
|
|
2
|
+
import { ChevronLeft, ChevronRight, Download, Grid3X3, List, Maximize, Monitor } from "lucide-react"
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
4
|
+
|
|
5
|
+
import type { SlideTransitionType } from "./transitions"
|
|
6
|
+
import type { SlideConfig } from "./types"
|
|
7
|
+
|
|
8
|
+
import { SLIDE_DIMENSIONS, SLIDE_TRANSITION } from "./animation-config"
|
|
9
|
+
import { AnimationProvider } from "./animation-context"
|
|
10
|
+
import { SlideErrorBoundary } from "./slide-error-boundary"
|
|
11
|
+
import { DEFAULT_SLIDE_TRANSITION, getSlideVariants } from "./transitions"
|
|
12
|
+
import { useSlideNavigation } from "./use-slide-navigation"
|
|
13
|
+
import { cn } from "./utils"
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
type ViewMode = "slide" | "list" | "grid"
|
|
20
|
+
|
|
21
|
+
interface SlideDeckProps {
|
|
22
|
+
slides: SlideConfig[]
|
|
23
|
+
transition?: SlideTransitionType
|
|
24
|
+
directionalTransition?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// COMPONENT
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export function SlideDeck({ slides, transition, directionalTransition }: SlideDeckProps) {
|
|
32
|
+
const [viewMode, setViewMode] = useState<ViewMode>("slide")
|
|
33
|
+
const [isPresentationMode, setIsPresentationMode] = useState(false)
|
|
34
|
+
const [scale, setScale] = useState(1)
|
|
35
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
currentSlide,
|
|
39
|
+
animationStep,
|
|
40
|
+
totalSteps,
|
|
41
|
+
direction,
|
|
42
|
+
showAllAnimations,
|
|
43
|
+
advance,
|
|
44
|
+
goBack,
|
|
45
|
+
goToSlide,
|
|
46
|
+
onTransitionComplete
|
|
47
|
+
} = useSlideNavigation({
|
|
48
|
+
slides
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const togglePresentationMode = useCallback(async () => {
|
|
52
|
+
if (!document.fullscreenElement) {
|
|
53
|
+
await document.documentElement.requestFullscreen()
|
|
54
|
+
} else {
|
|
55
|
+
await document.exitFullscreen()
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
// Listen for fullscreen changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const handleFullscreenChange = () => {
|
|
62
|
+
setIsPresentationMode(!!document.fullscreenElement)
|
|
63
|
+
}
|
|
64
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange)
|
|
65
|
+
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange)
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
// Calculate scale factor for presentation mode
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const calculateScale = () => {
|
|
71
|
+
if (!isPresentationMode) {
|
|
72
|
+
setScale(1)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const viewportWidth = window.innerWidth
|
|
76
|
+
const viewportHeight = window.innerHeight
|
|
77
|
+
const scaleX = viewportWidth / SLIDE_DIMENSIONS.width
|
|
78
|
+
const scaleY = viewportHeight / SLIDE_DIMENSIONS.height
|
|
79
|
+
setScale(Math.min(scaleX, scaleY))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
calculateScale()
|
|
83
|
+
window.addEventListener("resize", calculateScale)
|
|
84
|
+
return () => window.removeEventListener("resize", calculateScale)
|
|
85
|
+
}, [isPresentationMode])
|
|
86
|
+
|
|
87
|
+
const handleExportPdf = () => {
|
|
88
|
+
const previousMode = viewMode
|
|
89
|
+
setViewMode("list")
|
|
90
|
+
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
const handleAfterPrint = () => {
|
|
93
|
+
setViewMode(previousMode)
|
|
94
|
+
window.removeEventListener("afterprint", handleAfterPrint)
|
|
95
|
+
}
|
|
96
|
+
window.addEventListener("afterprint", handleAfterPrint)
|
|
97
|
+
window.print()
|
|
98
|
+
}, 100)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Keyboard navigation
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
104
|
+
if (e.key === "f" || e.key === "F") {
|
|
105
|
+
togglePresentationMode()
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// G for grid view toggle
|
|
110
|
+
if (e.key === "g" || e.key === "G") {
|
|
111
|
+
setViewMode(prev => (prev === "grid" ? "slide" : "grid"))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (viewMode !== "slide") return
|
|
116
|
+
|
|
117
|
+
if (e.key === "ArrowRight" || e.key === " ") {
|
|
118
|
+
e.preventDefault()
|
|
119
|
+
advance()
|
|
120
|
+
} else if (e.key === "ArrowLeft") {
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
goBack()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
window.addEventListener("keydown", handleKeyDown)
|
|
127
|
+
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
128
|
+
}, [advance, goBack, viewMode, togglePresentationMode])
|
|
129
|
+
|
|
130
|
+
// Per-slide transition resolution
|
|
131
|
+
const currentSlideTransition = slides[currentSlide]?.transition
|
|
132
|
+
const transitionType = currentSlideTransition ?? transition ?? DEFAULT_SLIDE_TRANSITION
|
|
133
|
+
const isDirectional = directionalTransition ?? false
|
|
134
|
+
|
|
135
|
+
const slideVariants = getSlideVariants(
|
|
136
|
+
{ type: transitionType, directional: isDirectional },
|
|
137
|
+
direction
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const CurrentSlideComponent = slides[currentSlide]!.component
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="min-h-screen w-full bg-neutral-950 text-foreground">
|
|
144
|
+
<style>{`
|
|
145
|
+
@media print {
|
|
146
|
+
@page {
|
|
147
|
+
size: 1920px 1080px;
|
|
148
|
+
margin: 0;
|
|
149
|
+
}
|
|
150
|
+
html,
|
|
151
|
+
body {
|
|
152
|
+
width: 100%;
|
|
153
|
+
height: 100%;
|
|
154
|
+
margin: 0 !important;
|
|
155
|
+
padding: 0 !important;
|
|
156
|
+
overflow: visible !important;
|
|
157
|
+
}
|
|
158
|
+
body {
|
|
159
|
+
print-color-adjust: exact;
|
|
160
|
+
-webkit-print-color-adjust: exact;
|
|
161
|
+
background: transparent !important;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
`}</style>
|
|
165
|
+
|
|
166
|
+
{/* Toolbar */}
|
|
167
|
+
<div
|
|
168
|
+
className={cn(
|
|
169
|
+
"fixed top-4 right-4 z-50 flex gap-1 rounded-lg border border-neutral-800 bg-neutral-950/90 p-1 backdrop-blur-sm print:hidden",
|
|
170
|
+
isPresentationMode && "hidden"
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => setViewMode("slide")}
|
|
175
|
+
className={cn(
|
|
176
|
+
"rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
177
|
+
viewMode === "slide" && "bg-neutral-800 text-white"
|
|
178
|
+
)}
|
|
179
|
+
title="Presentation View"
|
|
180
|
+
>
|
|
181
|
+
<Monitor className="h-4 w-4" />
|
|
182
|
+
</button>
|
|
183
|
+
<button
|
|
184
|
+
onClick={() => setViewMode("list")}
|
|
185
|
+
className={cn(
|
|
186
|
+
"rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
187
|
+
viewMode === "list" && "bg-neutral-800 text-white"
|
|
188
|
+
)}
|
|
189
|
+
title="List View"
|
|
190
|
+
>
|
|
191
|
+
<List className="h-4 w-4" />
|
|
192
|
+
</button>
|
|
193
|
+
<button
|
|
194
|
+
onClick={() => setViewMode("grid")}
|
|
195
|
+
className={cn(
|
|
196
|
+
"rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
197
|
+
viewMode === "grid" && "bg-neutral-800 text-white"
|
|
198
|
+
)}
|
|
199
|
+
title="Grid View"
|
|
200
|
+
>
|
|
201
|
+
<Grid3X3 className="h-4 w-4" />
|
|
202
|
+
</button>
|
|
203
|
+
|
|
204
|
+
<div className="mx-1 w-px bg-neutral-800" />
|
|
205
|
+
|
|
206
|
+
<button
|
|
207
|
+
onClick={handleExportPdf}
|
|
208
|
+
className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
|
|
209
|
+
title="Download PDF"
|
|
210
|
+
>
|
|
211
|
+
<Download className="h-4 w-4" />
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
onClick={togglePresentationMode}
|
|
215
|
+
className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
|
|
216
|
+
title="Present (F)"
|
|
217
|
+
>
|
|
218
|
+
<Maximize className="h-4 w-4" />
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{/* Slide View */}
|
|
223
|
+
{viewMode === "slide" && (
|
|
224
|
+
<div
|
|
225
|
+
ref={containerRef}
|
|
226
|
+
role="presentation"
|
|
227
|
+
tabIndex={isPresentationMode ? 0 : undefined}
|
|
228
|
+
className={cn(
|
|
229
|
+
"flex h-screen w-full flex-col items-center justify-center overflow-hidden print:hidden",
|
|
230
|
+
isPresentationMode ? "bg-black p-0" : "p-4 md:p-8"
|
|
231
|
+
)}
|
|
232
|
+
onClick={isPresentationMode ? advance : undefined}
|
|
233
|
+
onKeyDown={
|
|
234
|
+
isPresentationMode
|
|
235
|
+
? e => {
|
|
236
|
+
if (e.key === "Enter" || e.key === " ") advance()
|
|
237
|
+
}
|
|
238
|
+
: undefined
|
|
239
|
+
}
|
|
240
|
+
>
|
|
241
|
+
<LayoutGroup id="slide-deck">
|
|
242
|
+
{isPresentationMode ? (
|
|
243
|
+
<div
|
|
244
|
+
className="pointer-events-none relative overflow-hidden bg-black"
|
|
245
|
+
style={{
|
|
246
|
+
width: SLIDE_DIMENSIONS.width,
|
|
247
|
+
height: SLIDE_DIMENSIONS.height,
|
|
248
|
+
transform: `scale(${scale})`,
|
|
249
|
+
transformOrigin: "center center"
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<AnimatePresence initial={false}>
|
|
253
|
+
<motion.div
|
|
254
|
+
key={currentSlide}
|
|
255
|
+
variants={slideVariants}
|
|
256
|
+
initial="enter"
|
|
257
|
+
animate="center"
|
|
258
|
+
exit="exit"
|
|
259
|
+
transition={SLIDE_TRANSITION}
|
|
260
|
+
onAnimationComplete={definition => {
|
|
261
|
+
if (definition === "center") {
|
|
262
|
+
onTransitionComplete()
|
|
263
|
+
}
|
|
264
|
+
}}
|
|
265
|
+
className="absolute inset-0 h-full w-full"
|
|
266
|
+
>
|
|
267
|
+
<AnimationProvider
|
|
268
|
+
currentStep={animationStep}
|
|
269
|
+
totalSteps={totalSteps}
|
|
270
|
+
showAllAnimations={showAllAnimations}
|
|
271
|
+
>
|
|
272
|
+
<SlideErrorBoundary
|
|
273
|
+
slideIndex={currentSlide}
|
|
274
|
+
slideTitle={slides[currentSlide]?.title}
|
|
275
|
+
>
|
|
276
|
+
<CurrentSlideComponent
|
|
277
|
+
slideNumber={currentSlide + 1}
|
|
278
|
+
totalSlides={slides.length}
|
|
279
|
+
/>
|
|
280
|
+
</SlideErrorBoundary>
|
|
281
|
+
</AnimationProvider>
|
|
282
|
+
</motion.div>
|
|
283
|
+
</AnimatePresence>
|
|
284
|
+
</div>
|
|
285
|
+
) : (
|
|
286
|
+
<div className="relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl">
|
|
287
|
+
<AnimatePresence initial={false}>
|
|
288
|
+
<motion.div
|
|
289
|
+
key={currentSlide}
|
|
290
|
+
variants={slideVariants}
|
|
291
|
+
initial="enter"
|
|
292
|
+
animate="center"
|
|
293
|
+
exit="exit"
|
|
294
|
+
transition={SLIDE_TRANSITION}
|
|
295
|
+
onAnimationComplete={definition => {
|
|
296
|
+
if (definition === "center") {
|
|
297
|
+
onTransitionComplete()
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
300
|
+
className="absolute inset-0 h-full w-full"
|
|
301
|
+
>
|
|
302
|
+
<AnimationProvider
|
|
303
|
+
currentStep={animationStep}
|
|
304
|
+
totalSteps={totalSteps}
|
|
305
|
+
showAllAnimations={showAllAnimations}
|
|
306
|
+
>
|
|
307
|
+
<SlideErrorBoundary
|
|
308
|
+
slideIndex={currentSlide}
|
|
309
|
+
slideTitle={slides[currentSlide]?.title}
|
|
310
|
+
>
|
|
311
|
+
<CurrentSlideComponent
|
|
312
|
+
slideNumber={currentSlide + 1}
|
|
313
|
+
totalSlides={slides.length}
|
|
314
|
+
/>
|
|
315
|
+
</SlideErrorBoundary>
|
|
316
|
+
</AnimationProvider>
|
|
317
|
+
</motion.div>
|
|
318
|
+
</AnimatePresence>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
</LayoutGroup>
|
|
322
|
+
|
|
323
|
+
{/* Navigation Controls */}
|
|
324
|
+
{!isPresentationMode && (
|
|
325
|
+
<div className="mt-6 flex items-center gap-4">
|
|
326
|
+
<button
|
|
327
|
+
onClick={goBack}
|
|
328
|
+
className="rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white"
|
|
329
|
+
>
|
|
330
|
+
<ChevronLeft className="h-5 w-5" />
|
|
331
|
+
</button>
|
|
332
|
+
<div className="flex min-w-[4rem] flex-col items-center">
|
|
333
|
+
<span className="font-mono text-sm text-neutral-500">
|
|
334
|
+
{currentSlide + 1} / {slides.length}
|
|
335
|
+
</span>
|
|
336
|
+
{slides[currentSlide]?.title && (
|
|
337
|
+
<span className="mt-0.5 text-xs text-neutral-600">
|
|
338
|
+
{slides[currentSlide].title}
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
<button
|
|
343
|
+
onClick={advance}
|
|
344
|
+
className="rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white"
|
|
345
|
+
>
|
|
346
|
+
<ChevronRight className="h-5 w-5" />
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{/* Grid View */}
|
|
354
|
+
{viewMode === "grid" && (
|
|
355
|
+
<div className="mx-auto max-w-7xl p-8 pt-16 print:hidden">
|
|
356
|
+
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
|
357
|
+
{slides.map((slideConfig, index) => {
|
|
358
|
+
const SlideComponent = slideConfig.component
|
|
359
|
+
const prevSection = index > 0 ? slides[index - 1]?.section : undefined
|
|
360
|
+
const showSectionHeader = slideConfig.section && slideConfig.section !== prevSection
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div key={index} className={showSectionHeader ? "col-span-full" : undefined}>
|
|
364
|
+
{showSectionHeader && (
|
|
365
|
+
<h3 className="mt-4 mb-3 text-xs font-bold tracking-[0.2em] text-neutral-500 uppercase first:mt-0">
|
|
366
|
+
{slideConfig.section}
|
|
367
|
+
</h3>
|
|
368
|
+
)}
|
|
369
|
+
<button
|
|
370
|
+
onClick={() => {
|
|
371
|
+
goToSlide(index)
|
|
372
|
+
setViewMode("slide")
|
|
373
|
+
}}
|
|
374
|
+
className="group relative aspect-video w-full overflow-hidden rounded-lg border border-neutral-800 bg-black shadow-sm transition-all hover:border-primary hover:shadow-lg hover:shadow-primary/10"
|
|
375
|
+
>
|
|
376
|
+
<div
|
|
377
|
+
className="h-full w-full origin-top-left scale-[0.25]"
|
|
378
|
+
style={{ width: "400%", height: "400%" }}
|
|
379
|
+
>
|
|
380
|
+
<SlideErrorBoundary slideIndex={index} slideTitle={slideConfig.title}>
|
|
381
|
+
<SlideComponent slideNumber={index + 1} totalSlides={slides.length} />
|
|
382
|
+
</SlideErrorBoundary>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/20" />
|
|
385
|
+
<div className="absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs font-medium text-white">
|
|
386
|
+
{slideConfig.title ? `${index + 1}. ${slideConfig.title}` : index + 1}
|
|
387
|
+
</div>
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
)
|
|
391
|
+
})}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{/* List View */}
|
|
397
|
+
<div
|
|
398
|
+
className={cn(
|
|
399
|
+
"mx-auto max-w-7xl p-8 pt-16",
|
|
400
|
+
"print:m-0 print:block print:max-w-none print:p-0",
|
|
401
|
+
viewMode === "list" ? "block" : "hidden print:block"
|
|
402
|
+
)}
|
|
403
|
+
>
|
|
404
|
+
<div className="grid grid-cols-1 gap-8 print:block">
|
|
405
|
+
{slides.map((slideConfig, index) => {
|
|
406
|
+
const SlideComponent = slideConfig.component
|
|
407
|
+
return (
|
|
408
|
+
<div
|
|
409
|
+
key={index}
|
|
410
|
+
className="aspect-video w-full overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-sm print:relative print:m-0 print:h-[1080px] print:w-[1920px] print:break-after-page print:overflow-hidden print:rounded-none print:border-0 print:shadow-none"
|
|
411
|
+
>
|
|
412
|
+
<div className="h-full w-full print:h-[720px] print:w-[1280px] print:origin-top-left print:scale-[1.5]">
|
|
413
|
+
<AnimationProvider
|
|
414
|
+
currentStep={slideConfig.steps}
|
|
415
|
+
totalSteps={slideConfig.steps}
|
|
416
|
+
showAllAnimations={true}
|
|
417
|
+
>
|
|
418
|
+
<SlideErrorBoundary slideIndex={index} slideTitle={slideConfig.title}>
|
|
419
|
+
<SlideComponent slideNumber={index + 1} totalSlides={slides.length} />
|
|
420
|
+
</SlideErrorBoundary>
|
|
421
|
+
</AnimationProvider>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
})}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
interface SlideErrorBoundaryProps {
|
|
4
|
+
slideIndex: number
|
|
5
|
+
slideTitle?: string
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SlideErrorBoundaryState {
|
|
10
|
+
hasError: boolean
|
|
11
|
+
error: Error | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SlideErrorBoundary extends React.Component<
|
|
15
|
+
SlideErrorBoundaryProps,
|
|
16
|
+
SlideErrorBoundaryState
|
|
17
|
+
> {
|
|
18
|
+
state: SlideErrorBoundaryState = { hasError: false, error: null }
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError(error: Error) {
|
|
21
|
+
return { hasError: true, error }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
25
|
+
console.error(
|
|
26
|
+
`[PromptSlide] Slide ${this.props.slideIndex + 1}${this.props.slideTitle ? ` ("${this.props.slideTitle}")` : ""} crashed:`,
|
|
27
|
+
error,
|
|
28
|
+
errorInfo
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
if (this.state.hasError) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex h-full w-full flex-col items-center justify-center bg-red-950/20 p-12">
|
|
36
|
+
<div className="mb-4 text-lg font-semibold text-red-400">
|
|
37
|
+
Slide {this.props.slideIndex + 1} Error
|
|
38
|
+
{this.props.slideTitle && (
|
|
39
|
+
<span className="ml-2 font-normal text-red-400/70">({this.props.slideTitle})</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="max-w-2xl text-center font-mono text-sm break-words text-red-300/80">
|
|
43
|
+
{this.state.error?.message ?? "Unknown error"}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return this.props.children
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from "react"
|
|
2
|
+
import type { ThemeConfig } from "./types"
|
|
3
|
+
|
|
4
|
+
const ThemeContext = createContext<ThemeConfig | null>(null)
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Access the full theme config. Returns null outside SlideThemeProvider.
|
|
8
|
+
*/
|
|
9
|
+
export function useTheme(): ThemeConfig | null {
|
|
10
|
+
return useContext(ThemeContext)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildCssOverrides(theme: ThemeConfig): React.CSSProperties {
|
|
14
|
+
const style: Record<string, string> = {}
|
|
15
|
+
|
|
16
|
+
if (theme.colors?.primary) style["--primary"] = theme.colors.primary
|
|
17
|
+
if (theme.colors?.primaryForeground) style["--primary-foreground"] = theme.colors.primaryForeground
|
|
18
|
+
if (theme.colors?.secondary) style["--secondary"] = theme.colors.secondary
|
|
19
|
+
if (theme.colors?.secondaryForeground) style["--secondary-foreground"] = theme.colors.secondaryForeground
|
|
20
|
+
if (theme.colors?.accent) style["--accent"] = theme.colors.accent
|
|
21
|
+
if (theme.colors?.accentForeground) style["--accent-foreground"] = theme.colors.accentForeground
|
|
22
|
+
|
|
23
|
+
if (theme.fonts?.heading) style["--font-heading"] = theme.fonts.heading
|
|
24
|
+
if (theme.fonts?.body) style["--font-body"] = theme.fonts.body
|
|
25
|
+
|
|
26
|
+
return style as React.CSSProperties
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SlideThemeProviderProps {
|
|
30
|
+
theme: ThemeConfig
|
|
31
|
+
children: React.ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Provides the full theme to all descendants.
|
|
36
|
+
* Injects CSS variable overrides via inline style.
|
|
37
|
+
*/
|
|
38
|
+
export function SlideThemeProvider({ theme, children }: SlideThemeProviderProps) {
|
|
39
|
+
const cssOverrides = useMemo(() => buildCssOverrides(theme), [theme])
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ThemeContext.Provider value={theme}>
|
|
43
|
+
<div style={cssOverrides} className="contents">
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
</ThemeContext.Provider>
|
|
47
|
+
)
|
|
48
|
+
}
|