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.
Files changed (39) hide show
  1. package/dist/index.d.ts +377 -0
  2. package/dist/index.js +963 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +65 -0
  5. package/src/commands/build.mjs +73 -0
  6. package/src/commands/create.mjs +197 -0
  7. package/src/commands/preview.mjs +22 -0
  8. package/src/commands/studio.mjs +27 -0
  9. package/src/core/animated.tsx +153 -0
  10. package/src/core/animation-config.ts +98 -0
  11. package/src/core/animation-context.tsx +54 -0
  12. package/src/core/index.ts +73 -0
  13. package/src/core/layouts/shared-footer.tsx +43 -0
  14. package/src/core/morph.tsx +153 -0
  15. package/src/core/slide-deck.tsx +430 -0
  16. package/src/core/slide-error-boundary.tsx +50 -0
  17. package/src/core/theme-context.tsx +48 -0
  18. package/src/core/transitions.ts +200 -0
  19. package/src/core/types.ts +136 -0
  20. package/src/core/use-slide-navigation.ts +142 -0
  21. package/src/core/utils.ts +8 -0
  22. package/src/index.mjs +70 -0
  23. package/src/utils/ansi.mjs +5 -0
  24. package/src/utils/colors.mjs +44 -0
  25. package/src/utils/prompts.mjs +50 -0
  26. package/src/utils/tsconfig.mjs +35 -0
  27. package/src/vite/config.mjs +40 -0
  28. package/src/vite/plugin.mjs +66 -0
  29. package/templates/default/AGENTS.md +453 -0
  30. package/templates/default/README.md +35 -0
  31. package/templates/default/package.json +26 -0
  32. package/templates/default/public/logo.svg +7 -0
  33. package/templates/default/src/App.tsx +11 -0
  34. package/templates/default/src/deck-config.ts +8 -0
  35. package/templates/default/src/globals.css +157 -0
  36. package/templates/default/src/layouts/slide-layout-centered.tsx +59 -0
  37. package/templates/default/src/slides/slide-example.tsx +53 -0
  38. package/templates/default/src/slides/slide-title.tsx +27 -0
  39. 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
+ }