promptslide 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+
3
+ import type { SlideTransitionType } from "./transitions"
4
+ import type { SlideConfig } from "./types"
5
+
6
+ import { SLIDE_DIMENSIONS } from "./animation-config"
7
+ import { SlideRenderer } from "./slide-renderer"
8
+ import { useSlideNavigation } from "./use-slide-navigation"
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ interface SlideEmbedProps {
15
+ slides: SlideConfig[]
16
+ transition?: SlideTransitionType
17
+ directionalTransition?: boolean
18
+ }
19
+
20
+ // =============================================================================
21
+ // COMPONENT
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Headless slide viewer controlled via window.postMessage.
26
+ * Designed for embedding in an iframe (e.g. the registry editor preview).
27
+ *
28
+ * Inbound messages (parent → embed):
29
+ * { type: "navigate", data: { slide: number } }
30
+ * { type: "advance" }
31
+ * { type: "goBack" }
32
+ *
33
+ * Outbound messages (embed → parent):
34
+ * { type: "slideReady" }
35
+ * { type: "slideState", data: { currentSlide, totalSlides, animationStep, totalSteps, titles } }
36
+ * { type: "hmrUpdate" }
37
+ */
38
+ export function SlideEmbed({ slides, transition, directionalTransition }: SlideEmbedProps) {
39
+ const [scale, setScale] = useState(1)
40
+
41
+ const {
42
+ currentSlide,
43
+ animationStep,
44
+ totalSteps,
45
+ direction,
46
+ showAllAnimations,
47
+ advance,
48
+ goBack,
49
+ goToSlide,
50
+ onTransitionComplete
51
+ } = useSlideNavigation({ slides })
52
+
53
+ // Post slide state to parent whenever it changes
54
+ useEffect(() => {
55
+ const state = {
56
+ currentSlide,
57
+ totalSlides: slides.length,
58
+ animationStep,
59
+ totalSteps,
60
+ titles: slides.map(s => s.title || "")
61
+ }
62
+ window.parent.postMessage({ type: "slideState", data: state }, "*")
63
+ }, [currentSlide, animationStep, totalSteps, slides])
64
+
65
+ // Signal readiness on mount
66
+ useEffect(() => {
67
+ window.parent.postMessage({ type: "slideReady" }, "*")
68
+ }, [])
69
+
70
+ // Listen for Vite HMR updates
71
+ useEffect(() => {
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ const hot = (import.meta as any).hot as { on: (event: string, cb: () => void) => void } | undefined
74
+ if (hot) {
75
+ hot.on("vite:afterUpdate", () => {
76
+ window.parent.postMessage({ type: "hmrUpdate" }, "*")
77
+ })
78
+ }
79
+ }, [])
80
+
81
+ // Listen for inbound messages from parent
82
+ const handleMessage = useCallback(
83
+ (event: MessageEvent) => {
84
+ const { type, data } = event.data || {}
85
+ switch (type) {
86
+ case "navigate":
87
+ if (typeof data?.slide === "number") goToSlide(data.slide)
88
+ break
89
+ case "advance":
90
+ advance()
91
+ break
92
+ case "goBack":
93
+ goBack()
94
+ break
95
+ }
96
+ },
97
+ [advance, goBack, goToSlide]
98
+ )
99
+
100
+ useEffect(() => {
101
+ window.addEventListener("message", handleMessage)
102
+ return () => window.removeEventListener("message", handleMessage)
103
+ }, [handleMessage])
104
+
105
+ // Scale to fill viewport
106
+ useEffect(() => {
107
+ const calculateScale = () => {
108
+ const scaleX = window.innerWidth / SLIDE_DIMENSIONS.width
109
+ const scaleY = window.innerHeight / SLIDE_DIMENSIONS.height
110
+ setScale(Math.min(scaleX, scaleY))
111
+ }
112
+
113
+ calculateScale()
114
+ window.addEventListener("resize", calculateScale)
115
+ return () => window.removeEventListener("resize", calculateScale)
116
+ }, [])
117
+
118
+ return (
119
+ <div className="flex h-screen w-screen items-center justify-center overflow-hidden bg-black">
120
+ <div
121
+ className="relative overflow-hidden bg-black"
122
+ style={{
123
+ width: SLIDE_DIMENSIONS.width,
124
+ height: SLIDE_DIMENSIONS.height,
125
+ transform: `scale(${scale})`,
126
+ transformOrigin: "center center"
127
+ }}
128
+ >
129
+ <SlideRenderer
130
+ slides={slides}
131
+ currentSlide={currentSlide}
132
+ animationStep={animationStep}
133
+ totalSteps={totalSteps}
134
+ direction={direction}
135
+ showAllAnimations={showAllAnimations}
136
+ transition={transition}
137
+ directionalTransition={directionalTransition}
138
+ onTransitionComplete={onTransitionComplete}
139
+ />
140
+ </div>
141
+ </div>
142
+ )
143
+ }
@@ -0,0 +1,91 @@
1
+ import { AnimatePresence, motion } from "framer-motion"
2
+
3
+ import type { SlideTransitionType } from "./transitions"
4
+ import type { NavigationDirection, SlideConfig } from "./types"
5
+
6
+ import { SLIDE_TRANSITION } from "./animation-config"
7
+ import { AnimationProvider } from "./animation-context"
8
+ import { SlideErrorBoundary } from "./slide-error-boundary"
9
+ import { DEFAULT_SLIDE_TRANSITION, getSlideVariants } from "./transitions"
10
+
11
+ // =============================================================================
12
+ // TYPES
13
+ // =============================================================================
14
+
15
+ export interface SlideRendererProps {
16
+ slides: SlideConfig[]
17
+ currentSlide: number
18
+ animationStep: number
19
+ totalSteps: number
20
+ direction: NavigationDirection
21
+ showAllAnimations: boolean
22
+ transition?: SlideTransitionType
23
+ directionalTransition?: boolean
24
+ onTransitionComplete: () => void
25
+ }
26
+
27
+ // =============================================================================
28
+ // COMPONENT
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Renders a single slide with animated transitions.
33
+ * Extracted from SlideDeck so it can be reused in SlideEmbed and other contexts.
34
+ */
35
+ export function SlideRenderer({
36
+ slides,
37
+ currentSlide,
38
+ animationStep,
39
+ totalSteps,
40
+ direction,
41
+ showAllAnimations,
42
+ transition,
43
+ directionalTransition,
44
+ onTransitionComplete
45
+ }: SlideRendererProps) {
46
+ const currentSlideTransition = slides[currentSlide]?.transition
47
+ const transitionType = currentSlideTransition ?? transition ?? DEFAULT_SLIDE_TRANSITION
48
+ const isDirectional = directionalTransition ?? false
49
+
50
+ const slideVariants = getSlideVariants(
51
+ { type: transitionType, directional: isDirectional },
52
+ direction
53
+ )
54
+
55
+ const CurrentSlideComponent = slides[currentSlide]!.component
56
+
57
+ return (
58
+ <AnimatePresence initial={false}>
59
+ <motion.div
60
+ key={currentSlide}
61
+ variants={slideVariants}
62
+ initial="enter"
63
+ animate="center"
64
+ exit="exit"
65
+ transition={SLIDE_TRANSITION}
66
+ onAnimationComplete={definition => {
67
+ if (definition === "center") {
68
+ onTransitionComplete()
69
+ }
70
+ }}
71
+ className="absolute inset-0 h-full w-full"
72
+ >
73
+ <AnimationProvider
74
+ currentStep={animationStep}
75
+ totalSteps={totalSteps}
76
+ showAllAnimations={showAllAnimations}
77
+ >
78
+ <SlideErrorBoundary
79
+ slideIndex={currentSlide}
80
+ slideTitle={slides[currentSlide]?.title}
81
+ >
82
+ <CurrentSlideComponent
83
+ slideNumber={currentSlide + 1}
84
+ totalSlides={slides.length}
85
+ />
86
+ </SlideErrorBoundary>
87
+ </AnimationProvider>
88
+ </motion.div>
89
+ </AnimatePresence>
90
+ )
91
+ }
@@ -2,6 +2,8 @@ const VIRTUAL_ENTRY_ID = "virtual:promptslide-entry"
2
2
  const RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID
3
3
  const VIRTUAL_EXPORT_ID = "virtual:promptslide-export"
4
4
  const RESOLVED_VIRTUAL_EXPORT_ID = "\0" + VIRTUAL_EXPORT_ID
5
+ const VIRTUAL_EMBED_ID = "virtual:promptslide-embed"
6
+ const RESOLVED_VIRTUAL_EMBED_ID = "\0" + VIRTUAL_EMBED_ID
5
7
 
6
8
  function getHtmlTemplate() {
7
9
  return `<!doctype html>
@@ -89,6 +91,45 @@ createRoot(document.getElementById("root")).render(
89
91
  `
90
92
  }
91
93
 
94
+ function getEmbedHtmlTemplate() {
95
+ return `<!doctype html>
96
+ <html lang="en" class="dark">
97
+ <head>
98
+ <meta charset="UTF-8" />
99
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
100
+ <title>PromptSlide Embed</title>
101
+ </head>
102
+ <body>
103
+ <div id="root"></div>
104
+ <script type="module" src="/@id/${VIRTUAL_EMBED_ID}"></script>
105
+ </body>
106
+ </html>`
107
+ }
108
+
109
+ function getEmbedEntryModule(root) {
110
+ return `
111
+ import { StrictMode, createElement } from "react"
112
+ import { createRoot } from "react-dom/client"
113
+ import { SlideEmbed, SlideThemeProvider } from "promptslide"
114
+ import "${root}/src/globals.css"
115
+ import { slides } from "${root}/src/deck-config"
116
+
117
+ let theme = {}
118
+ try {
119
+ const themeMod = await import("${root}/src/theme")
120
+ theme = themeMod.theme || themeMod.default || {}
121
+ } catch {}
122
+
123
+ createRoot(document.getElementById("root")).render(
124
+ createElement(StrictMode, null,
125
+ createElement(SlideThemeProvider, { theme },
126
+ createElement(SlideEmbed, { slides })
127
+ )
128
+ )
129
+ )
130
+ `
131
+ }
132
+
92
133
  export function promptslidePlugin({ root: initialRoot } = {}) {
93
134
  let root = initialRoot
94
135
  let exportSlidePath = null
@@ -104,14 +145,27 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
104
145
  resolveId(id) {
105
146
  if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ENTRY_ID
106
147
  if (id === VIRTUAL_EXPORT_ID) return RESOLVED_VIRTUAL_EXPORT_ID
148
+ if (id === VIRTUAL_EMBED_ID) return RESOLVED_VIRTUAL_EMBED_ID
107
149
  },
108
150
 
109
151
  load(id) {
110
152
  if (id === RESOLVED_VIRTUAL_ENTRY_ID) return getEntryModule(root)
111
153
  if (id === RESOLVED_VIRTUAL_EXPORT_ID) return getExportEntryModule(root, exportSlidePath || "src/slides/slide-title.tsx")
154
+ if (id === RESOLVED_VIRTUAL_EMBED_ID) return getEmbedEntryModule(root)
112
155
  },
113
156
 
114
157
  configureServer(server) {
158
+ // Pre-middleware: serve /embed route
159
+ server.middlewares.use(async (req, res, next) => {
160
+ const url = new URL(req.url, "http://localhost")
161
+ if (url.pathname !== "/embed" && url.pathname !== "/embed/") return next()
162
+
163
+ const html = await server.transformIndexHtml("/embed", getEmbedHtmlTemplate())
164
+ res.setHeader("Content-Type", "text/html")
165
+ res.statusCode = 200
166
+ res.end(html)
167
+ })
168
+
115
169
  // Pre-middleware: intercept export URLs before Vite's SPA fallback rewrites them
116
170
  server.middlewares.use(async (req, res, next) => {
117
171
  const url = new URL(req.url, "http://localhost")
@@ -9,7 +9,7 @@
9
9
  "preview": "promptslide preview"
10
10
  },
11
11
  "dependencies": {
12
- "promptslide": "^0.2.0",
12
+ "promptslide": "{{PROMPTSLIDE_VERSION}}",
13
13
  "clsx": "^2.1.1",
14
14
  "framer-motion": "^12.23.22",
15
15
  "lucide-react": "^0.552.0",
@@ -48,7 +48,7 @@ export function SlideLayoutCentered({
48
48
  )}
49
49
 
50
50
  {/* Content Area */}
51
- <div className="min-h-0 w-full flex-1 overflow-hidden pt-2">{children}</div>
51
+ <div className="flex min-h-0 w-full flex-1 flex-col justify-center overflow-hidden">{children}</div>
52
52
 
53
53
  {/* Footer */}
54
54
  {!hideFooter && (