promptslide 0.2.4 → 0.2.6

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,141 @@
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
+ setScale(window.innerWidth / SLIDE_DIMENSIONS.width)
109
+ }
110
+
111
+ calculateScale()
112
+ window.addEventListener("resize", calculateScale)
113
+ return () => window.removeEventListener("resize", calculateScale)
114
+ }, [])
115
+
116
+ return (
117
+ <div className="flex h-screen w-screen items-center justify-center overflow-hidden bg-black">
118
+ <div
119
+ className="relative overflow-hidden bg-black"
120
+ style={{
121
+ width: SLIDE_DIMENSIONS.width,
122
+ height: SLIDE_DIMENSIONS.height,
123
+ transform: `scale(${scale})`,
124
+ transformOrigin: "center center"
125
+ }}
126
+ >
127
+ <SlideRenderer
128
+ slides={slides}
129
+ currentSlide={currentSlide}
130
+ animationStep={animationStep}
131
+ totalSteps={totalSteps}
132
+ direction={direction}
133
+ showAllAnimations={showAllAnimations}
134
+ transition={transition}
135
+ directionalTransition={directionalTransition}
136
+ onTransitionComplete={onTransitionComplete}
137
+ />
138
+ </div>
139
+ </div>
140
+ )
141
+ }
@@ -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/index.mjs CHANGED
@@ -31,6 +31,7 @@ function printHelp() {
31
31
  console.log(` add ${dim("<name>")} Install a slide/deck from the registry`)
32
32
  console.log(` publish ${dim("[file]")} Publish a slide to the registry`)
33
33
  console.log(` update ${dim("[name]")} Check for and apply updates`)
34
+ console.log(` pull Pull the latest deck from the registry`)
34
35
  console.log(` remove ${dim("<name>")} Remove an installed item`)
35
36
  console.log(` info ${dim("<name>")} Show details about a registry item`)
36
37
  console.log(` search ${dim("<query>")} Search the registry`)
@@ -99,6 +100,11 @@ switch (command) {
99
100
  await update(args)
100
101
  break
101
102
  }
103
+ case "pull": {
104
+ const { pull } = await import("./commands/pull.mjs")
105
+ await pull(args)
106
+ break
107
+ }
102
108
  case "search": {
103
109
  const { search } = await import("./commands/search.mjs")
104
110
  await search(args)
@@ -20,7 +20,8 @@ export function loadAuth() {
20
20
  registry: data.registry || DEFAULT_REGISTRY,
21
21
  token: data.token,
22
22
  organizationId: data.organizationId || null,
23
- organizationName: data.organizationName || null
23
+ organizationName: data.organizationName || null,
24
+ organizationSlug: data.organizationSlug || null
24
25
  }
25
26
  } catch {
26
27
  return null
@@ -30,13 +31,14 @@ export function loadAuth() {
30
31
  /**
31
32
  * Save auth credentials to ~/.promptslide/auth.json.
32
33
  */
33
- export function saveAuth({ registry, token, organizationId, organizationName }) {
34
+ export function saveAuth({ registry, token, organizationId, organizationName, organizationSlug }) {
34
35
  mkdirSync(AUTH_DIR, { recursive: true })
35
36
  const data = {
36
37
  registry: registry || DEFAULT_REGISTRY,
37
38
  token,
38
39
  organizationId: organizationId || null,
39
- organizationName: organizationName || null
40
+ organizationName: organizationName || null,
41
+ organizationSlug: organizationSlug || null
40
42
  }
41
43
  writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 })
42
44
  }
@@ -52,3 +52,73 @@ export function confirm(question, defaultYes = true) {
52
52
  })
53
53
  })
54
54
  }
55
+
56
+ /**
57
+ * Arrow-key select prompt. Returns the index of the chosen option.
58
+ * @param {string[]} options - Display labels for each option
59
+ * @param {number} defaultIndex - Initially highlighted index
60
+ * @returns {Promise<number>}
61
+ */
62
+ export function select(options, defaultIndex = 0) {
63
+ if (!process.stdin.isTTY) return Promise.resolve(defaultIndex)
64
+
65
+ // Pause readline so we can use raw mode
66
+ if (_rl) { _rl.pause() }
67
+
68
+ return new Promise(resolve => {
69
+ let cursor = defaultIndex
70
+ const { stdin, stdout } = process
71
+
72
+ const render = () => {
73
+ // Move up to overwrite previous render (except first time)
74
+ if (render._drawn) stdout.write(`\x1b[${options.length}A`)
75
+ for (let i = 0; i < options.length; i++) {
76
+ const prefix = i === cursor ? " \x1b[36m❯\x1b[0m " : " "
77
+ const label = i === cursor ? `\x1b[1m${options[i]}\x1b[0m` : dim(options[i])
78
+ stdout.write(`\x1b[2K${prefix}${label}\n`)
79
+ }
80
+ render._drawn = true
81
+ }
82
+
83
+ stdin.setRawMode(true)
84
+ stdin.resume()
85
+ render()
86
+
87
+ const onData = (buf) => {
88
+ const key = buf.toString()
89
+
90
+ // Ctrl+C
91
+ if (key === "\x03") {
92
+ stdin.setRawMode(false)
93
+ stdin.removeListener("data", onData)
94
+ if (_rl) { _rl.resume() }
95
+ process.exit(0)
96
+ }
97
+
98
+ // Up arrow or k
99
+ if (key === "\x1b[A" || key === "k") {
100
+ cursor = (cursor - 1 + options.length) % options.length
101
+ render()
102
+ return
103
+ }
104
+
105
+ // Down arrow or j
106
+ if (key === "\x1b[B" || key === "j") {
107
+ cursor = (cursor + 1) % options.length
108
+ render()
109
+ return
110
+ }
111
+
112
+ // Enter
113
+ if (key === "\r" || key === "\n") {
114
+ stdin.setRawMode(false)
115
+ stdin.removeListener("data", onData)
116
+ stdin.pause()
117
+ if (_rl) { _rl.resume() }
118
+ resolve(cursor)
119
+ }
120
+ }
121
+
122
+ stdin.on("data", onData)
123
+ })
124
+ }
@@ -79,6 +79,18 @@ export function updateLockfileItem(cwd, slug, version, files) {
79
79
  writeLockfile(cwd, lock)
80
80
  }
81
81
 
82
+ /**
83
+ * Store publish configuration (deckPrefix, deckSlug) in the lockfile.
84
+ * @param {string} cwd
85
+ * @param {{ deckPrefix?: string, deckSlug?: string }} config
86
+ */
87
+ export function updateLockfilePublishConfig(cwd, config) {
88
+ const lock = readLockfile(cwd)
89
+ if (config.deckPrefix !== undefined) lock.deckPrefix = config.deckPrefix
90
+ if (config.deckSlug !== undefined) lock.deckSlug = config.deckSlug
91
+ writeLockfile(cwd, lock)
92
+ }
93
+
82
94
  /**
83
95
  * Remove a single item from the lockfile.
84
96
  * @param {string} cwd
@@ -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 && (