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.
- package/dist/index.d.ts +39 -1
- package/dist/index.js +243 -130
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/create.mjs +6 -37
- package/src/commands/studio.mjs +8 -1
- package/src/core/index.ts +7 -0
- package/src/core/slide-deck.tsx +34 -82
- package/src/core/slide-embed.tsx +143 -0
- package/src/core/slide-renderer.tsx +91 -0
- package/src/vite/plugin.mjs +54 -0
- package/templates/default/package.json +1 -1
- package/templates/default/src/layouts/slide-layout-centered.tsx +1 -1
|
@@ -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
|
+
}
|
package/src/vite/plugin.mjs
CHANGED
|
@@ -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")
|
|
@@ -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
|
|
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 && (
|