promptslide 0.3.16 → 0.3.18
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/CHANGELOG.md +14 -0
- package/README.md +16 -12
- package/dist/index.d.ts +17 -7
- package/dist/index.js +1072 -225
- package/dist/index.js.map +1 -1
- package/package.json +31 -31
- package/src/commands/clone.mjs +54 -21
- package/src/commands/pull.mjs +57 -27
- package/src/commands/studio.mjs +7 -3
- package/src/commands/to-image.mjs +18 -5
- package/src/commands/to-pdf.mjs +27 -0
- package/src/core/animated.tsx +3 -3
- package/src/core/annotations/adapters/http.ts +8 -0
- package/src/core/annotations/annotation-overlay.tsx +77 -32
- package/src/core/annotations/annotation-panel.tsx +184 -51
- package/src/core/annotations/types.ts +5 -0
- package/src/core/annotations/use-annotations.ts +57 -7
- package/src/core/slide-deck.tsx +740 -116
- package/src/core/slide-renderer.tsx +3 -8
- package/src/core/transitions.ts +5 -3
- package/src/core/types.ts +2 -0
- package/src/core/use-slide-navigation.ts +93 -3
- package/src/editor/README.md +35 -0
- package/src/editor/geometry.ts +76 -0
- package/src/editor/mount.ts +24 -0
- package/src/editor/plugin.mjs +189 -0
- package/src/editor/source.mjs +363 -0
- package/src/editor/studio.css +1004 -0
- package/src/editor/studio.tsx +1948 -0
- package/src/index.mjs +6 -0
- package/src/utils/deck-config.mjs +30 -11
- package/src/utils/export.mjs +151 -52
- package/src/utils/studio-discovery.mjs +37 -0
- package/src/vite/config.mjs +23 -2
- package/src/vite/plugin.mjs +147 -16
- package/templates/default/AGENTS.md +3 -2
- package/templates/default/src/deck-config.ts +2 -2
package/src/vite/plugin.mjs
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs"
|
|
2
|
-
import { join } from "node:path"
|
|
2
|
+
import { join, resolve, sep, extname } from "node:path"
|
|
3
|
+
|
|
3
4
|
import { bold, dim } from "../utils/ansi.mjs"
|
|
5
|
+
import { projectIdentity } from "../utils/studio-discovery.mjs"
|
|
4
6
|
|
|
5
7
|
/** Normalize a path to forward slashes so it can be used in ES module import strings on Windows. */
|
|
6
8
|
function toImportPath(p) {
|
|
7
9
|
return p.split("\\").join("/")
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
function validateSlidePath(root, slidePath) {
|
|
13
|
+
const absolute = resolve(root, slidePath)
|
|
14
|
+
if (
|
|
15
|
+
!absolute.startsWith(resolve(root) + sep) ||
|
|
16
|
+
![".tsx", ".jsx", ".ts", ".js"].includes(extname(absolute)) ||
|
|
17
|
+
!existsSync(absolute)
|
|
18
|
+
) {
|
|
19
|
+
throw new Error(`Invalid slide path: ${slidePath}`)
|
|
20
|
+
}
|
|
21
|
+
return absolute
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
const VIRTUAL_ENTRY_ID = "virtual:promptslide-entry"
|
|
11
25
|
const RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID
|
|
12
26
|
const VIRTUAL_EXPORT_ID = "virtual:promptslide-export"
|
|
13
27
|
const RESOLVED_VIRTUAL_EXPORT_ID = "\0" + VIRTUAL_EXPORT_ID
|
|
14
28
|
const VIRTUAL_EMBED_ID = "virtual:promptslide-embed"
|
|
15
29
|
const RESOLVED_VIRTUAL_EMBED_ID = "\0" + VIRTUAL_EMBED_ID
|
|
30
|
+
const VIRTUAL_PDF_ID = "virtual:promptslide-pdf"
|
|
31
|
+
const RESOLVED_VIRTUAL_PDF_ID = "\0" + VIRTUAL_PDF_ID
|
|
16
32
|
|
|
17
33
|
// Inline script that catches module load errors (e.g. missing named exports)
|
|
18
34
|
// and forwards them to the Vite dev server so they appear in terminal logs.
|
|
@@ -45,7 +61,8 @@ function getHtmlTemplate() {
|
|
|
45
61
|
</html>`
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
function getExportHtmlTemplate() {
|
|
64
|
+
function getExportHtmlTemplate(slidePath) {
|
|
65
|
+
const entry = `${VIRTUAL_EXPORT_ID}?slidePath=${encodeURIComponent(slidePath)}`
|
|
49
66
|
return `<!doctype html>
|
|
50
67
|
<html lang="en">
|
|
51
68
|
<head>
|
|
@@ -55,7 +72,7 @@ function getExportHtmlTemplate() {
|
|
|
55
72
|
</head>
|
|
56
73
|
<body>
|
|
57
74
|
<div id="root"></div>
|
|
58
|
-
<script type="module" src="/@id/${
|
|
75
|
+
<script type="module" src="/@id/${entry}"></script>
|
|
59
76
|
</body>
|
|
60
77
|
</html>`
|
|
61
78
|
}
|
|
@@ -76,12 +93,13 @@ createRoot(document.getElementById("root")).render(
|
|
|
76
93
|
|
|
77
94
|
function getExportEntryModule(root, slidePath) {
|
|
78
95
|
const r = toImportPath(root)
|
|
96
|
+
const slideImport = JSON.stringify(toImportPath(validateSlidePath(root, slidePath)))
|
|
79
97
|
return `
|
|
80
98
|
import { StrictMode, createElement, useState, useEffect } from "react"
|
|
81
99
|
import { createRoot } from "react-dom/client"
|
|
82
100
|
import { AnimationProvider, SlideErrorBoundary, SlideThemeProvider } from "promptslide"
|
|
83
101
|
import "${r}/src/globals.css"
|
|
84
|
-
import * as slideMod from
|
|
102
|
+
import * as slideMod from ${slideImport}
|
|
85
103
|
|
|
86
104
|
let theme = {}
|
|
87
105
|
try {
|
|
@@ -93,7 +111,18 @@ const SlideComponent = slideMod.default || Object.values(slideMod).find(v => typ
|
|
|
93
111
|
|
|
94
112
|
function ExportView() {
|
|
95
113
|
const [ready, setReady] = useState(false)
|
|
96
|
-
useEffect(() => {
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
let cancelled = false
|
|
116
|
+
const markReady = async () => {
|
|
117
|
+
await document.fonts.ready
|
|
118
|
+
await Promise.all(Array.from(document.images, image => image.decode().catch(() => {})))
|
|
119
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
120
|
+
if (!cancelled) setReady(true)
|
|
121
|
+
}))
|
|
122
|
+
}
|
|
123
|
+
void markReady()
|
|
124
|
+
return () => { cancelled = true }
|
|
125
|
+
}, [])
|
|
97
126
|
return createElement("div", {
|
|
98
127
|
"data-export-ready": ready ? "true" : undefined,
|
|
99
128
|
style: { width: 1280, height: 720, overflow: "hidden", position: "relative", background: "black" }
|
|
@@ -134,6 +163,63 @@ function getEmbedHtmlTemplate() {
|
|
|
134
163
|
</html>`
|
|
135
164
|
}
|
|
136
165
|
|
|
166
|
+
function getPdfHtmlTemplate() {
|
|
167
|
+
return `<!doctype html><html><head><meta charset="UTF-8" /><title>PromptSlide PDF</title></head>
|
|
168
|
+
<body><div id="root"></div><script type="module" src="/@id/${VIRTUAL_PDF_ID}"></script></body></html>`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getPdfEntryModule(root) {
|
|
172
|
+
const r = toImportPath(root)
|
|
173
|
+
return `
|
|
174
|
+
import { StrictMode, createElement, useEffect, useState } from "react"
|
|
175
|
+
import { createRoot } from "react-dom/client"
|
|
176
|
+
import { AnimationProvider, SlideErrorBoundary, SlideThemeProvider } from "promptslide"
|
|
177
|
+
import "${r}/src/globals.css"
|
|
178
|
+
import { slides } from "${r}/src/deck-config"
|
|
179
|
+
|
|
180
|
+
let theme = {}
|
|
181
|
+
try {
|
|
182
|
+
const themeMod = await import("${r}/src/theme")
|
|
183
|
+
theme = themeMod.theme || themeMod.default || {}
|
|
184
|
+
} catch {}
|
|
185
|
+
|
|
186
|
+
const style = document.createElement("style")
|
|
187
|
+
style.textContent = "@page{size:1280px 720px;margin:0}html,body,#root{margin:0;padding:0;width:1280px;background:#000}body{print-color-adjust:exact;-webkit-print-color-adjust:exact}.pdf-slide{width:1280px;height:720px;overflow:hidden;break-after:page;page-break-after:always}.pdf-slide:last-child{break-after:auto}"
|
|
188
|
+
document.head.appendChild(style)
|
|
189
|
+
|
|
190
|
+
function PdfView() {
|
|
191
|
+
const [ready, setReady] = useState(false)
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
let cancelled = false
|
|
194
|
+
const markReady = async () => {
|
|
195
|
+
await document.fonts.ready
|
|
196
|
+
await Promise.all(Array.from(document.images, image => image.decode().catch(() => {})))
|
|
197
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
198
|
+
if (!cancelled) setReady(true)
|
|
199
|
+
}))
|
|
200
|
+
}
|
|
201
|
+
void markReady()
|
|
202
|
+
return () => { cancelled = true }
|
|
203
|
+
}, [])
|
|
204
|
+
return createElement("div", { "data-pdf-ready": ready ? "true" : undefined },
|
|
205
|
+
slides.map((slide, index) => createElement("div", { className: "pdf-slide", key: slide.id || index },
|
|
206
|
+
createElement(AnimationProvider, { currentStep: slide.steps, totalSteps: slide.steps, showAllAnimations: true },
|
|
207
|
+
createElement(SlideErrorBoundary, { slideIndex: index, slideTitle: slide.title },
|
|
208
|
+
createElement(slide.component, { slideNumber: index + 1, totalSlides: slides.length })
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
))
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
createRoot(document.getElementById("root")).render(
|
|
216
|
+
createElement(StrictMode, null,
|
|
217
|
+
createElement(SlideThemeProvider, { theme }, createElement(PdfView))
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
`
|
|
221
|
+
}
|
|
222
|
+
|
|
137
223
|
function getEmbedEntryModule(root) {
|
|
138
224
|
const r = toImportPath(root)
|
|
139
225
|
return `
|
|
@@ -161,7 +247,6 @@ createRoot(document.getElementById("root")).render(
|
|
|
161
247
|
|
|
162
248
|
export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
163
249
|
let root = initialRoot
|
|
164
|
-
let exportSlidePath = null
|
|
165
250
|
|
|
166
251
|
return {
|
|
167
252
|
name: "promptslide",
|
|
@@ -183,28 +268,57 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
|
183
268
|
|
|
184
269
|
resolveId(id) {
|
|
185
270
|
if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ENTRY_ID
|
|
186
|
-
if (id
|
|
271
|
+
if (id.startsWith(`${VIRTUAL_EXPORT_ID}?`)) return "\0" + id
|
|
187
272
|
if (id === VIRTUAL_EMBED_ID) return RESOLVED_VIRTUAL_EMBED_ID
|
|
273
|
+
if (id === VIRTUAL_PDF_ID) return RESOLVED_VIRTUAL_PDF_ID
|
|
188
274
|
},
|
|
189
275
|
|
|
190
276
|
load(id) {
|
|
191
277
|
if (id === RESOLVED_VIRTUAL_ENTRY_ID) return getEntryModule(root)
|
|
192
|
-
if (id
|
|
278
|
+
if (id.startsWith(`${RESOLVED_VIRTUAL_EXPORT_ID}?`)) {
|
|
279
|
+
const params = new URLSearchParams(id.slice(id.indexOf("?") + 1))
|
|
280
|
+
return getExportEntryModule(root, params.get("slidePath") || "src/slides/slide-title.tsx")
|
|
281
|
+
}
|
|
193
282
|
if (id === RESOLVED_VIRTUAL_EMBED_ID) return getEmbedEntryModule(root)
|
|
283
|
+
if (id === RESOLVED_VIRTUAL_PDF_ID) return getPdfEntryModule(root)
|
|
194
284
|
},
|
|
195
285
|
|
|
196
286
|
configureServer(server) {
|
|
287
|
+
server.middlewares.use(async (req, res, next) => {
|
|
288
|
+
if (req.method !== "GET" || req.url !== "/__promptslide_pdf") return next()
|
|
289
|
+
try {
|
|
290
|
+
const { captureDeckPdfFromServer } = await import("../utils/export.mjs")
|
|
291
|
+
const address = server.httpServer.address()
|
|
292
|
+
const port = typeof address === "object" ? address.port : 0
|
|
293
|
+
const pdf = await captureDeckPdfFromServer(`http://127.0.0.1:${port}`)
|
|
294
|
+
res.setHeader("Content-Type", "application/pdf")
|
|
295
|
+
res.setHeader("Content-Disposition", 'attachment; filename="slides.pdf"')
|
|
296
|
+
res.end(pdf)
|
|
297
|
+
} catch (error) {
|
|
298
|
+
res.statusCode = 500
|
|
299
|
+
res.end(String(error.message || error))
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
server.middlewares.use((req, res, next) => {
|
|
303
|
+
if (req.method !== "GET" || req.url !== "/__promptslide_info") return next()
|
|
304
|
+
res.setHeader("Content-Type", "application/json")
|
|
305
|
+
res.end(JSON.stringify({ projectId: projectIdentity(root) }))
|
|
306
|
+
})
|
|
197
307
|
// Pre-middleware: receive browser errors and log them to the terminal
|
|
198
308
|
server.middlewares.use((req, res, next) => {
|
|
199
309
|
if (req.method !== "POST" || req.url !== "/__promptslide_error") return next()
|
|
200
310
|
|
|
201
311
|
let body = ""
|
|
202
|
-
req.on("data", chunk => {
|
|
312
|
+
req.on("data", chunk => {
|
|
313
|
+
body += chunk
|
|
314
|
+
})
|
|
203
315
|
req.on("end", () => {
|
|
204
316
|
try {
|
|
205
317
|
const { message, filename } = JSON.parse(body)
|
|
206
318
|
const location = filename ? ` ${dim(`(${filename})`)}` : ""
|
|
207
|
-
server.config.logger.error(`${bold("Browser error:")} ${message}${location}`, {
|
|
319
|
+
server.config.logger.error(`${bold("Browser error:")} ${message}${location}`, {
|
|
320
|
+
timestamp: true
|
|
321
|
+
})
|
|
208
322
|
} catch {}
|
|
209
323
|
res.statusCode = 204
|
|
210
324
|
res.end()
|
|
@@ -238,7 +352,9 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
|
238
352
|
if (req.method !== "POST" || req.url !== "/__promptslide_annotations") return next()
|
|
239
353
|
|
|
240
354
|
let body = ""
|
|
241
|
-
req.on("data", chunk => {
|
|
355
|
+
req.on("data", chunk => {
|
|
356
|
+
body += chunk
|
|
357
|
+
})
|
|
242
358
|
req.on("end", () => {
|
|
243
359
|
try {
|
|
244
360
|
const data = JSON.parse(body)
|
|
@@ -267,13 +383,28 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
|
267
383
|
// Pre-middleware: intercept export URLs before Vite's SPA fallback rewrites them
|
|
268
384
|
server.middlewares.use(async (req, res, next) => {
|
|
269
385
|
const url = new URL(req.url, "http://localhost")
|
|
386
|
+
if (url.searchParams.get("pdf") === "true") {
|
|
387
|
+
const html = await server.transformIndexHtml("/index.html", getPdfHtmlTemplate(), req.url)
|
|
388
|
+
res.setHeader("Content-Type", "text/html")
|
|
389
|
+
res.statusCode = 200
|
|
390
|
+
res.end(html)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
270
393
|
if (url.searchParams.get("export") !== "true") return next()
|
|
271
394
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
395
|
+
const slidePath = url.searchParams.get("slidePath") || "src/slides/slide-title.tsx"
|
|
396
|
+
try {
|
|
397
|
+
validateSlidePath(root, slidePath)
|
|
398
|
+
} catch (error) {
|
|
399
|
+
res.statusCode = 400
|
|
400
|
+
res.end(error.message)
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
const html = await server.transformIndexHtml(
|
|
404
|
+
"/index.html",
|
|
405
|
+
getExportHtmlTemplate(slidePath),
|
|
406
|
+
req.url
|
|
407
|
+
)
|
|
277
408
|
res.setHeader("Content-Type", "text/html")
|
|
278
409
|
res.statusCode = 200
|
|
279
410
|
res.end(html)
|
|
@@ -73,8 +73,8 @@ import { SlideTitle } from "@/slides/slide-title";
|
|
|
73
73
|
import { SlideMarket } from "@/slides/slide-market";
|
|
74
74
|
|
|
75
75
|
export const slides: SlideConfig[] = [
|
|
76
|
-
{ component: SlideTitle, steps: 0 },
|
|
77
|
-
{ component: SlideMarket, steps: 0 },
|
|
76
|
+
{ id: "title", component: SlideTitle, steps: 0 },
|
|
77
|
+
{ id: "market", component: SlideMarket, steps: 0 },
|
|
78
78
|
];
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -85,6 +85,7 @@ Layouts in `src/layouts/` and theme colors in `src/globals.css` are yours to cus
|
|
|
85
85
|
## Key Constraints
|
|
86
86
|
|
|
87
87
|
- **Slide dimensions**: 1280×720 (16:9). Content scales automatically in presentation mode.
|
|
88
|
+
- **Slide IDs**: Give each slide a unique, stable `id` in `deck-config.ts` so links and annotations survive reordering. Existing decks without IDs remain supported.
|
|
88
89
|
- **Semantic colors**: Use `text-foreground`, `text-muted-foreground`, `text-primary`, `bg-background`, `bg-card`, `border-border`.
|
|
89
90
|
- **Icons**: Import from `lucide-react` (e.g., `import { ArrowRight } from "lucide-react"`).
|
|
90
91
|
- **Animations**: Use `<Animated step={n}>` for click-to-reveal. The `steps` value in `deck-config.ts` must equal the highest step number used. Available: `fade`, `slide-up`, `slide-down`, `slide-left`, `slide-right`, `scale`. **Important:** `<Animated>` renders a wrapper div — when inside a grid/flex container, pass layout classes (`h-full`, `col-span-*`, etc.) via `className` on the `<Animated>`, not only on the inner child.
|
|
@@ -3,6 +3,6 @@ import { SlideTitle } from "@/slides/slide-title";
|
|
|
3
3
|
import { SlideExample } from "@/slides/slide-example";
|
|
4
4
|
|
|
5
5
|
export const slides: SlideConfig[] = [
|
|
6
|
-
{ component: SlideTitle, steps: 0 },
|
|
7
|
-
{ component: SlideExample, steps: 2 },
|
|
6
|
+
{ id: "title", component: SlideTitle, steps: 0 },
|
|
7
|
+
{ id: "example", component: SlideExample, steps: 2 },
|
|
8
8
|
];
|