promptslide 0.2.2 → 0.2.4
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/package.json +1 -1
- package/src/commands/add.mjs +18 -4
- package/src/commands/create.mjs +97 -10
- package/src/commands/publish.mjs +567 -17
- package/src/utils/auth.mjs +1 -1
- package/src/utils/deck-config.mjs +55 -0
- package/src/utils/prompts.mjs +6 -2
- package/src/utils/registry.mjs +128 -18
- package/templates/default/AGENTS.md +7 -1
- package/templates/default/public/.gitkeep +0 -0
- package/templates/default/src/theme.ts +1 -4
- package/templates/default/public/logo.svg +0 -7
package/package.json
CHANGED
package/src/commands/add.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, mkdirSync } from "node:fs"
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
2
2
|
import { execFileSync } from "node:child_process"
|
|
3
3
|
import { join, dirname, resolve, sep } from "node:path"
|
|
4
4
|
|
|
@@ -85,8 +85,17 @@ export async function add(args) {
|
|
|
85
85
|
const newHash = hashContent(file.content)
|
|
86
86
|
|
|
87
87
|
if (existsSync(targetPath)) {
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// For binary files (data URIs), compare decoded bytes directly
|
|
89
|
+
const dataUriMatch = file.content.match(/^data:[^;]+;base64,/)
|
|
90
|
+
let identical
|
|
91
|
+
if (dataUriMatch) {
|
|
92
|
+
const newBuf = Buffer.from(file.content.slice(dataUriMatch[0].length), "base64")
|
|
93
|
+
const existingBuf = readFileSync(targetPath)
|
|
94
|
+
identical = newBuf.equals(existingBuf)
|
|
95
|
+
} else {
|
|
96
|
+
identical = hashFile(targetPath) === newHash
|
|
97
|
+
}
|
|
98
|
+
if (identical) {
|
|
90
99
|
console.log(` ${dim("Skipped")} ${relativePath} ${dim("(identical)")}`)
|
|
91
100
|
fileHashes[relativePath] = newHash
|
|
92
101
|
continue
|
|
@@ -99,7 +108,12 @@ export async function add(args) {
|
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
mkdirSync(targetDir, { recursive: true })
|
|
102
|
-
|
|
111
|
+
const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
|
|
112
|
+
if (dataUriPrefix) {
|
|
113
|
+
writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
|
|
114
|
+
} else {
|
|
115
|
+
writeFileSync(targetPath, file.content, "utf-8")
|
|
116
|
+
}
|
|
103
117
|
fileHashes[relativePath] = newHash
|
|
104
118
|
written.push({ item: regItem, file })
|
|
105
119
|
console.log(` ${green("✓")} Added ${cyan(relativePath)}${regItem !== item ? dim(" (dependency)") : ""}`)
|
package/src/commands/create.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { execSync } from "node:child_process"
|
|
2
|
-
import { existsSync, cpSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { existsSync, cpSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
3
3
|
import { join, resolve, dirname } from "node:path"
|
|
4
4
|
import { fileURLToPath } from "node:url"
|
|
5
5
|
|
|
6
6
|
import { bold, green, cyan, red, dim } from "../utils/ansi.mjs"
|
|
7
|
+
import { requireAuth } from "../utils/auth.mjs"
|
|
7
8
|
import { hexToOklch, isValidHex } from "../utils/colors.mjs"
|
|
8
9
|
import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
|
|
10
|
+
import { fetchRegistryItem, resolveRegistryDependencies } from "../utils/registry.mjs"
|
|
11
|
+
import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
|
|
9
12
|
import { ensureTsConfig } from "../utils/tsconfig.mjs"
|
|
10
13
|
|
|
11
14
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -58,7 +61,9 @@ export async function create(args) {
|
|
|
58
61
|
|
|
59
62
|
// Parse flags
|
|
60
63
|
const useDefaults = args.includes("--yes") || args.includes("-y")
|
|
61
|
-
const
|
|
64
|
+
const fromIdx = args.findIndex(a => a === "--from")
|
|
65
|
+
const fromSlug = fromIdx >= 0 ? args[fromIdx + 1] : null
|
|
66
|
+
const filteredArgs = args.filter((a, i) => a !== "--yes" && a !== "-y" && a !== "--from" && !(fromIdx >= 0 && i === fromIdx + 1))
|
|
62
67
|
|
|
63
68
|
// 1. Parse directory name from args or prompt
|
|
64
69
|
let dirName = filteredArgs[0]
|
|
@@ -69,11 +74,13 @@ export async function create(args) {
|
|
|
69
74
|
console.log(` Scaffolds a new PromptSlide slide deck project.`)
|
|
70
75
|
console.log()
|
|
71
76
|
console.log(` ${bold("Options:")}`)
|
|
72
|
-
console.log(` -y, --yes
|
|
77
|
+
console.log(` -y, --yes Skip prompts and use defaults`)
|
|
78
|
+
console.log(` --from <deck-slug> Start from a published deck`)
|
|
73
79
|
console.log()
|
|
74
|
-
console.log(` ${bold("
|
|
80
|
+
console.log(` ${bold("Examples:")}`)
|
|
75
81
|
console.log(` promptslide create my-pitch-deck`)
|
|
76
82
|
console.log(` promptslide create my-pitch-deck --yes`)
|
|
83
|
+
console.log(` promptslide create my-deck --from promptic-pitch-deck`)
|
|
77
84
|
console.log()
|
|
78
85
|
process.exit(0)
|
|
79
86
|
}
|
|
@@ -109,9 +116,9 @@ export async function create(args) {
|
|
|
109
116
|
const defaultName = titleCase(dirName)
|
|
110
117
|
const projectName = useDefaults ? defaultName : await prompt("Project name:", defaultName)
|
|
111
118
|
|
|
112
|
-
// 3. Ask for primary brand color (optional)
|
|
119
|
+
// 3. Ask for primary brand color (optional, skipped when using --from since deck provides its own)
|
|
113
120
|
let primaryHex = "#3B82F6"
|
|
114
|
-
if (!useDefaults) {
|
|
121
|
+
if (!useDefaults && !fromSlug) {
|
|
115
122
|
primaryHex = await prompt("Primary brand color (hex):", "#3B82F6")
|
|
116
123
|
if (!isValidHex(primaryHex)) {
|
|
117
124
|
console.log(` ${dim("Invalid hex color, using default #3B82F6")}`)
|
|
@@ -161,7 +168,87 @@ export async function create(args) {
|
|
|
161
168
|
replaceInFile(path, values)
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
// 6.
|
|
171
|
+
// 6. Overlay deck files if --from was specified
|
|
172
|
+
if (fromSlug) {
|
|
173
|
+
const auth = requireAuth()
|
|
174
|
+
|
|
175
|
+
let item
|
|
176
|
+
try {
|
|
177
|
+
item = await fetchRegistryItem(fromSlug, auth)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(` ${red("Error:")} ${err.message}`)
|
|
180
|
+
closePrompts()
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (item.type !== "deck") {
|
|
185
|
+
console.error(` ${red("Error:")} "${fromSlug}" is a ${item.type}, not a deck. Use --from with a published deck.`)
|
|
186
|
+
closePrompts()
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const versionTag = item.version ? ` ${dim(`v${item.version}`)}` : ""
|
|
191
|
+
console.log(` Using deck ${bold(item.title || item.name)}${versionTag}`)
|
|
192
|
+
|
|
193
|
+
let resolved
|
|
194
|
+
try {
|
|
195
|
+
resolved = await resolveRegistryDependencies(item, auth, targetDir)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(` ${red("Error:")} ${err.message}`)
|
|
198
|
+
closePrompts()
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Write all deck files (no conflict prompts — fresh project)
|
|
203
|
+
for (const regItem of resolved.items) {
|
|
204
|
+
if (!regItem.files?.length) continue
|
|
205
|
+
for (const file of regItem.files) {
|
|
206
|
+
const targetPath = join(targetDir, file.target, file.path)
|
|
207
|
+
const targetFileDir = dirname(targetPath)
|
|
208
|
+
mkdirSync(targetFileDir, { recursive: true })
|
|
209
|
+
|
|
210
|
+
const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
|
|
211
|
+
if (dataUriPrefix) {
|
|
212
|
+
writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
|
|
213
|
+
} else {
|
|
214
|
+
writeFileSync(targetPath, file.content, "utf-8")
|
|
215
|
+
}
|
|
216
|
+
console.log(` ${green("✓")} Added ${cyan(file.target + file.path)}`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Reconstruct deck-config.ts from deckConfig metadata
|
|
221
|
+
if (item.meta?.slides) {
|
|
222
|
+
const slides = item.meta.slides.map(s => ({
|
|
223
|
+
componentName: toPascalCase(s.slug),
|
|
224
|
+
importPath: `@/slides/${s.slug}`,
|
|
225
|
+
steps: s.steps,
|
|
226
|
+
section: s.section
|
|
227
|
+
}))
|
|
228
|
+
replaceDeckConfig(targetDir, slides, {
|
|
229
|
+
transition: item.meta.transition,
|
|
230
|
+
directionalTransition: item.meta.directionalTransition
|
|
231
|
+
})
|
|
232
|
+
console.log(` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Add npm dependencies from the deck to package.json
|
|
236
|
+
if (Object.keys(resolved.npmDeps).length > 0) {
|
|
237
|
+
const pkgList = Object.entries(resolved.npmDeps).map(([name, ver]) => `${name}@${ver}`)
|
|
238
|
+
const pkgPath = join(targetDir, "package.json")
|
|
239
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
|
|
240
|
+
for (const [name, ver] of Object.entries(resolved.npmDeps)) {
|
|
241
|
+
pkg.dependencies = pkg.dependencies || {}
|
|
242
|
+
pkg.dependencies[name] = ver
|
|
243
|
+
}
|
|
244
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8")
|
|
245
|
+
console.log(` ${green("✓")} Added ${dim(pkgList.join(", "))} to package.json`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 7. If running from local dev, rewrite deps to use file: paths (must run after --from to not overwrite)
|
|
165
252
|
const localPaths = getLocalPackagePaths()
|
|
166
253
|
if (localPaths) {
|
|
167
254
|
const pkgPath = join(targetDir, "package.json")
|
|
@@ -171,10 +258,10 @@ export async function create(args) {
|
|
|
171
258
|
console.log(` ${dim("Local dev detected — using file: paths for packages")}`)
|
|
172
259
|
}
|
|
173
260
|
|
|
174
|
-
//
|
|
261
|
+
// 8. Generate tsconfig.json for editor support
|
|
175
262
|
ensureTsConfig(targetDir)
|
|
176
263
|
|
|
177
|
-
//
|
|
264
|
+
// 9. Install PromptSlide agent skill (defaults to yes; skipped with --yes since skills CLI is interactive)
|
|
178
265
|
if (useDefaults) {
|
|
179
266
|
console.log(` ${dim("Tip: Run")} npx skills add prompticeu/promptslide ${dim("to install the agent skill")}`)
|
|
180
267
|
} else {
|
|
@@ -196,7 +283,7 @@ export async function create(args) {
|
|
|
196
283
|
}
|
|
197
284
|
}
|
|
198
285
|
|
|
199
|
-
//
|
|
286
|
+
// 10. Success output
|
|
200
287
|
console.log()
|
|
201
288
|
console.log(` ${green("✓")} Created ${bold(projectName)} in ${cyan(dirName)}/`)
|
|
202
289
|
console.log()
|
package/src/commands/publish.mjs
CHANGED
|
@@ -4,8 +4,33 @@ import { join, basename, relative, extname } from "node:path"
|
|
|
4
4
|
import { bold, green, cyan, red, dim } from "../utils/ansi.mjs"
|
|
5
5
|
import { requireAuth } from "../utils/auth.mjs"
|
|
6
6
|
import { captureSlideAsDataUri, isPlaywrightAvailable } from "../utils/export.mjs"
|
|
7
|
-
import { publishToRegistry, registryItemExists, updateLockfileItem, hashContent, detectPackageManager } from "../utils/registry.mjs"
|
|
7
|
+
import { publishToRegistry, registryItemExists, searchRegistry, updateLockfileItem, readLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent } from "../utils/registry.mjs"
|
|
8
8
|
import { prompt, confirm, closePrompts } from "../utils/prompts.mjs"
|
|
9
|
+
import { parseDeckConfig } from "../utils/deck-config.mjs"
|
|
10
|
+
|
|
11
|
+
function readDeckPrefix(cwd) {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"))
|
|
14
|
+
return (pkg.name || "").toLowerCase()
|
|
15
|
+
} catch {
|
|
16
|
+
return ""
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function promptDeckPrefix(cwd, interactive) {
|
|
21
|
+
const defaultPrefix = readDeckPrefix(cwd)
|
|
22
|
+
if (!interactive) {
|
|
23
|
+
if (!defaultPrefix) throw new Error("Deck prefix is required. Set a name in package.json or publish interactively.")
|
|
24
|
+
return defaultPrefix
|
|
25
|
+
}
|
|
26
|
+
let prefix
|
|
27
|
+
while (true) {
|
|
28
|
+
prefix = await prompt("Deck prefix:", defaultPrefix)
|
|
29
|
+
if (prefix && /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(prefix)) break
|
|
30
|
+
console.log(` ${red("Error:")} Deck prefix is required (lowercase alphanumeric with hyphens, min 2 chars)`)
|
|
31
|
+
}
|
|
32
|
+
return prefix
|
|
33
|
+
}
|
|
9
34
|
|
|
10
35
|
function titleCase(slug) {
|
|
11
36
|
return slug
|
|
@@ -69,6 +94,171 @@ function readPreviewImage(imagePath) {
|
|
|
69
94
|
return `data:${mime};base64,${buf.toString("base64")}`
|
|
70
95
|
}
|
|
71
96
|
|
|
97
|
+
const BINARY_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico", ".woff", ".woff2"])
|
|
98
|
+
const MAX_INLINE_SIZE = 512_000 // 512KB for inline text content
|
|
99
|
+
const MAX_DECK_FILES = 50 // registry limit
|
|
100
|
+
|
|
101
|
+
const MIME_MAP = {
|
|
102
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
103
|
+
".webp": "image/webp", ".gif": "image/gif", ".ico": "image/x-icon",
|
|
104
|
+
".woff": "font/woff", ".woff2": "font/woff2"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read a file for registry publishing.
|
|
109
|
+
* Returns { binary: true, buffer, contentType, size } for binary files,
|
|
110
|
+
* or { binary: false, content } for text files.
|
|
111
|
+
*/
|
|
112
|
+
function readFileForRegistry(fullPath) {
|
|
113
|
+
const ext = extname(fullPath).toLowerCase()
|
|
114
|
+
if (BINARY_EXTS.has(ext)) {
|
|
115
|
+
const buffer = readFileSync(fullPath)
|
|
116
|
+
const contentType = MIME_MAP[ext] || "application/octet-stream"
|
|
117
|
+
return { binary: true, buffer, contentType, size: buffer.length }
|
|
118
|
+
}
|
|
119
|
+
return { binary: false, content: readFileSync(fullPath, "utf-8") }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Collect all files for deck publishing.
|
|
124
|
+
* Returns files with structured format: text files have { path, target, content },
|
|
125
|
+
* binary files have { path, target, binary: true, buffer, contentType, size }.
|
|
126
|
+
*/
|
|
127
|
+
function collectDeckFiles(cwd) {
|
|
128
|
+
const files = []
|
|
129
|
+
const warnings = []
|
|
130
|
+
|
|
131
|
+
function addDir(dirPath, target, filter) {
|
|
132
|
+
if (!existsSync(dirPath)) return
|
|
133
|
+
for (const entry of readdirSync(dirPath)) {
|
|
134
|
+
if (entry === ".gitkeep") continue
|
|
135
|
+
const fullPath = join(dirPath, entry)
|
|
136
|
+
const stat = statSync(fullPath)
|
|
137
|
+
if (stat.isDirectory()) {
|
|
138
|
+
addDir(fullPath, `${target}${entry}/`, filter)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if (!stat.isFile()) continue
|
|
142
|
+
if (filter && !filter(entry)) continue
|
|
143
|
+
const fileData = readFileForRegistry(fullPath)
|
|
144
|
+
if (fileData.binary) {
|
|
145
|
+
files.push({ path: entry, target, binary: true, buffer: fileData.buffer, contentType: fileData.contentType, size: fileData.size })
|
|
146
|
+
} else {
|
|
147
|
+
if (fileData.content.length > MAX_INLINE_SIZE) {
|
|
148
|
+
warnings.push(`${target}${entry}: exceeds 512KB inline limit (${(fileData.content.length / 1024).toFixed(0)}KB), skipped`)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
files.push({ path: entry, target, content: fileData.content })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
addDir(join(cwd, "src", "slides"), "src/slides/", f => f.endsWith(".tsx") || f.endsWith(".ts"))
|
|
157
|
+
addDir(join(cwd, "src", "layouts"), "src/layouts/", f => f.endsWith(".tsx") || f.endsWith(".ts"))
|
|
158
|
+
|
|
159
|
+
const themePath = join(cwd, "src", "theme.ts")
|
|
160
|
+
if (existsSync(themePath)) {
|
|
161
|
+
files.push({ path: "theme.ts", target: "src/", content: readFileSync(themePath, "utf-8") })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const globalsPath = join(cwd, "src", "globals.css")
|
|
165
|
+
if (existsSync(globalsPath)) {
|
|
166
|
+
files.push({ path: "globals.css", target: "src/", content: readFileSync(globalsPath, "utf-8") })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
addDir(join(cwd, "public"), "public/")
|
|
170
|
+
|
|
171
|
+
if (files.length > MAX_DECK_FILES) {
|
|
172
|
+
warnings.push(`Deck has ${files.length} files but registry limit is ${MAX_DECK_FILES}. Only the first ${MAX_DECK_FILES} will be included.`)
|
|
173
|
+
files.length = MAX_DECK_FILES
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { files, warnings }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Upload binary files directly to Vercel Blob via pre-signed tokens,
|
|
181
|
+
* then return a unified file array ready for the publish payload.
|
|
182
|
+
*
|
|
183
|
+
* Text files get { path, target, content }.
|
|
184
|
+
* Binary files get { path, target, storageUrl, contentType } (or fall back to inline data URIs).
|
|
185
|
+
*
|
|
186
|
+
* @param {string} slug - Item slug
|
|
187
|
+
* @param {object[]} files - Mixed array from collectDeckFiles
|
|
188
|
+
* @param {{ registry: string, token: string }} auth
|
|
189
|
+
* @returns {Promise<object[]>} Files ready for publish payload
|
|
190
|
+
*/
|
|
191
|
+
async function uploadBinaryFiles(slug, files, auth) {
|
|
192
|
+
const binaryFiles = files.filter(f => f.binary)
|
|
193
|
+
const textFiles = files.filter(f => !f.binary)
|
|
194
|
+
|
|
195
|
+
if (binaryFiles.length === 0) {
|
|
196
|
+
return textFiles.map(f => ({ path: f.path, target: f.target, content: f.content }))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Request upload tokens from registry
|
|
200
|
+
let tokens = []
|
|
201
|
+
try {
|
|
202
|
+
tokens = await requestUploadTokens(
|
|
203
|
+
slug,
|
|
204
|
+
binaryFiles.map(f => ({ path: f.path, contentType: f.contentType, size: f.size })),
|
|
205
|
+
auth
|
|
206
|
+
)
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(` ${dim("⚠ Could not get upload tokens, falling back to inline encoding")}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = textFiles.map(f => ({ path: f.path, target: f.target, content: f.content }))
|
|
212
|
+
|
|
213
|
+
if (tokens.length === 0) {
|
|
214
|
+
// Fallback: encode binary files as base64 data URIs inline
|
|
215
|
+
for (const f of binaryFiles) {
|
|
216
|
+
result.push({
|
|
217
|
+
path: f.path,
|
|
218
|
+
target: f.target,
|
|
219
|
+
content: `data:${f.contentType};base64,${f.buffer.toString("base64")}`
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
return result
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Upload each binary file directly to Vercel Blob
|
|
226
|
+
const tokenMap = Object.fromEntries(tokens.map(t => [t.path, t]))
|
|
227
|
+
|
|
228
|
+
for (const f of binaryFiles) {
|
|
229
|
+
const token = tokenMap[f.path]
|
|
230
|
+
if (!token) {
|
|
231
|
+
// No token for this file — fall back to inline
|
|
232
|
+
result.push({
|
|
233
|
+
path: f.path,
|
|
234
|
+
target: f.target,
|
|
235
|
+
content: `data:${f.contentType};base64,${f.buffer.toString("base64")}`
|
|
236
|
+
})
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const blobUrl = await uploadBinaryToBlob(f.buffer, token.pathname, f.contentType, token.clientToken)
|
|
242
|
+
result.push({
|
|
243
|
+
path: f.path,
|
|
244
|
+
target: f.target,
|
|
245
|
+
storageUrl: blobUrl,
|
|
246
|
+
contentType: f.contentType
|
|
247
|
+
})
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.log(` ${dim(`⚠ Direct upload failed for ${f.path}: ${err.message}`)}`)
|
|
250
|
+
// Fall back to inline
|
|
251
|
+
result.push({
|
|
252
|
+
path: f.path,
|
|
253
|
+
target: f.target,
|
|
254
|
+
content: `data:${f.contentType};base64,${f.buffer.toString("base64")}`
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
|
|
72
262
|
function scanForFiles(cwd) {
|
|
73
263
|
const files = []
|
|
74
264
|
const dirs = [
|
|
@@ -88,10 +278,10 @@ function scanForFiles(cwd) {
|
|
|
88
278
|
|
|
89
279
|
/**
|
|
90
280
|
* Publish a single item to the registry.
|
|
91
|
-
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean }} opts
|
|
281
|
+
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean, deckPrefix?: string }} opts
|
|
92
282
|
* @returns {Promise<{ slug: string, status: string }>}
|
|
93
283
|
*/
|
|
94
|
-
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true }) {
|
|
284
|
+
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true, deckPrefix }) {
|
|
95
285
|
const fullPath = join(cwd, filePath)
|
|
96
286
|
if (!existsSync(fullPath)) {
|
|
97
287
|
throw new Error(`File not found: ${filePath}`)
|
|
@@ -99,7 +289,9 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
99
289
|
|
|
100
290
|
const content = readFileSync(fullPath, "utf-8")
|
|
101
291
|
const fileName = basename(fullPath)
|
|
102
|
-
const
|
|
292
|
+
const baseSlug = fileName.replace(/\.tsx?$/, "")
|
|
293
|
+
const prefix = deckPrefix || await promptDeckPrefix(cwd, interactive)
|
|
294
|
+
const slug = `${prefix}/${baseSlug}`
|
|
103
295
|
|
|
104
296
|
const type = typeOverride || detectType(filePath) || "slide"
|
|
105
297
|
const steps = detectSteps(content)
|
|
@@ -110,7 +302,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
110
302
|
let title, description, tags, section, releaseNotes, previewImage
|
|
111
303
|
|
|
112
304
|
if (interactive) {
|
|
113
|
-
title = await prompt("Title:", titleCase(
|
|
305
|
+
title = await prompt("Title:", titleCase(baseSlug))
|
|
114
306
|
description = await prompt("Description:", "")
|
|
115
307
|
const tagsInput = await prompt("Tags (comma-separated):", "")
|
|
116
308
|
tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
@@ -133,7 +325,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
133
325
|
}
|
|
134
326
|
}
|
|
135
327
|
} else {
|
|
136
|
-
title = titleCase(
|
|
328
|
+
title = titleCase(baseSlug)
|
|
137
329
|
description = ""
|
|
138
330
|
tags = []
|
|
139
331
|
section = ""
|
|
@@ -141,6 +333,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
141
333
|
previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath }).catch(() => null)
|
|
142
334
|
}
|
|
143
335
|
|
|
336
|
+
const prefixedDeps = registryDeps.map(d => `${prefix}/${d}`)
|
|
144
337
|
const payload = {
|
|
145
338
|
type,
|
|
146
339
|
slug,
|
|
@@ -151,7 +344,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
151
344
|
section: section || undefined,
|
|
152
345
|
files: [{ path: fileName, target, content }],
|
|
153
346
|
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
154
|
-
registryDependencies:
|
|
347
|
+
registryDependencies: prefixedDeps.length ? prefixedDeps : undefined,
|
|
155
348
|
releaseNotes: releaseNotes || undefined,
|
|
156
349
|
previewImage: previewImage || undefined
|
|
157
350
|
}
|
|
@@ -175,11 +368,12 @@ export async function publish(args) {
|
|
|
175
368
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
176
369
|
console.log(` ${bold("Usage:")} promptslide publish ${dim("[file] [--type slide|layout|deck|theme]")}`)
|
|
177
370
|
console.log()
|
|
178
|
-
console.log(` Publish a slide or
|
|
371
|
+
console.log(` Publish a slide, layout, or entire deck to the registry.`)
|
|
179
372
|
console.log()
|
|
180
373
|
console.log(` ${bold("Examples:")}`)
|
|
181
374
|
console.log(` promptslide publish src/slides/slide-hero.tsx`)
|
|
182
375
|
console.log(` promptslide publish --type layout`)
|
|
376
|
+
console.log(` promptslide publish --type deck`)
|
|
183
377
|
console.log()
|
|
184
378
|
process.exit(0)
|
|
185
379
|
}
|
|
@@ -200,7 +394,7 @@ export async function publish(args) {
|
|
|
200
394
|
}
|
|
201
395
|
let filePath = args.find((a, i) => !a.startsWith("--") && !flagIndices.has(i))
|
|
202
396
|
|
|
203
|
-
if (!filePath) {
|
|
397
|
+
if (!filePath && typeOverride !== "deck") {
|
|
204
398
|
// Interactive mode: scan for files
|
|
205
399
|
const available = scanForFiles(cwd)
|
|
206
400
|
if (available.length === 0) {
|
|
@@ -212,16 +406,348 @@ export async function publish(args) {
|
|
|
212
406
|
available.forEach((f, i) => {
|
|
213
407
|
console.log(` ${dim(`${i + 1}.`)} ${f.target}${f.path}`)
|
|
214
408
|
})
|
|
409
|
+
console.log(` ${dim("─".repeat(30))}`)
|
|
410
|
+
console.log(` ${dim(`${available.length + 1}.`)} ${bold("Entire deck")}`)
|
|
215
411
|
console.log()
|
|
216
412
|
|
|
217
413
|
const choice = await prompt("Select file number:", "1")
|
|
218
414
|
const idx = parseInt(choice, 10) - 1
|
|
219
|
-
|
|
415
|
+
|
|
416
|
+
if (idx === available.length) {
|
|
417
|
+
typeOverride = "deck"
|
|
418
|
+
} else if (idx < 0 || idx >= available.length) {
|
|
220
419
|
console.error(` ${red("Error:")} Invalid selection.`)
|
|
221
420
|
process.exit(1)
|
|
421
|
+
} else {
|
|
422
|
+
filePath = relative(cwd, available[idx].fullPath)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Deck publish flow (decomposed) ───────────────────────────────
|
|
427
|
+
if (typeOverride === "deck") {
|
|
428
|
+
const deckConfig = parseDeckConfig(cwd)
|
|
429
|
+
if (!deckConfig) {
|
|
430
|
+
console.error(` ${red("Error:")} Could not parse src/deck-config.ts.`)
|
|
431
|
+
console.error(` ${dim("Ensure it has a slides array with component/steps entries.")}`)
|
|
432
|
+
process.exit(1)
|
|
222
433
|
}
|
|
223
434
|
|
|
224
|
-
|
|
435
|
+
const deckPrefix = await promptDeckPrefix(cwd, true)
|
|
436
|
+
const dirName = basename(cwd)
|
|
437
|
+
const deckBaseSlug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
|
|
438
|
+
const deckSlug = `${deckPrefix}/${deckBaseSlug}`
|
|
439
|
+
|
|
440
|
+
// Walk public/ to collect assets and build reference set
|
|
441
|
+
const publicDir = join(cwd, "public")
|
|
442
|
+
const publicAssets = []
|
|
443
|
+
const publicFileSet = new Set()
|
|
444
|
+
function walkPublic(dir, prefix) {
|
|
445
|
+
if (!existsSync(dir)) return
|
|
446
|
+
for (const entry of readdirSync(dir)) {
|
|
447
|
+
if (entry.startsWith(".")) continue
|
|
448
|
+
const full = join(dir, entry)
|
|
449
|
+
const s = statSync(full)
|
|
450
|
+
if (s.isDirectory()) {
|
|
451
|
+
walkPublic(full, prefix ? `${prefix}/${entry}` : entry)
|
|
452
|
+
} else if (s.isFile()) {
|
|
453
|
+
const relPath = prefix ? `${prefix}/${entry}` : entry
|
|
454
|
+
publicAssets.push({ relativePath: relPath, fullPath: full })
|
|
455
|
+
publicFileSet.add(relPath)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
walkPublic(publicDir, "")
|
|
460
|
+
|
|
461
|
+
// Discover theme, layout, and slide files
|
|
462
|
+
const themeFilePaths = []
|
|
463
|
+
if (existsSync(join(cwd, "src", "theme.ts"))) themeFilePaths.push("theme.ts")
|
|
464
|
+
if (existsSync(join(cwd, "src", "globals.css"))) themeFilePaths.push("globals.css")
|
|
465
|
+
const hasTheme = themeFilePaths.length > 0
|
|
466
|
+
|
|
467
|
+
const layoutsDir = join(cwd, "src", "layouts")
|
|
468
|
+
const layoutEntries = existsSync(layoutsDir)
|
|
469
|
+
? readdirSync(layoutsDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
|
|
470
|
+
: []
|
|
471
|
+
|
|
472
|
+
const slidesDir = join(cwd, "src", "slides")
|
|
473
|
+
const slideEntries = existsSync(slidesDir)
|
|
474
|
+
? readdirSync(slidesDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
|
|
475
|
+
: []
|
|
476
|
+
|
|
477
|
+
const totalItems = publicAssets.length + (hasTheme ? 1 : 0) + layoutEntries.length + slideEntries.length + 1
|
|
478
|
+
|
|
479
|
+
// Display summary
|
|
480
|
+
console.log(` ${bold("Decomposed deck publish:")} ${cyan(deckSlug)}`)
|
|
481
|
+
if (publicAssets.length) console.log(` Assets: ${publicAssets.length}`)
|
|
482
|
+
if (hasTheme) console.log(` Theme: 1`)
|
|
483
|
+
if (layoutEntries.length) console.log(` Layouts: ${layoutEntries.length}`)
|
|
484
|
+
console.log(` Slides: ${slideEntries.length}`)
|
|
485
|
+
console.log(` Total: ${totalItems} items`)
|
|
486
|
+
if (deckConfig.transition) {
|
|
487
|
+
console.log(` Transition: ${deckConfig.transition}${deckConfig.directionalTransition ? " (directional)" : ""}`)
|
|
488
|
+
}
|
|
489
|
+
console.log()
|
|
490
|
+
|
|
491
|
+
// Collect deck metadata
|
|
492
|
+
const title = await prompt("Title:", titleCase(deckBaseSlug))
|
|
493
|
+
const description = await prompt("Description:", "")
|
|
494
|
+
const tagsInput = await prompt("Tags (comma-separated):", "")
|
|
495
|
+
const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
496
|
+
const releaseNotes = await prompt("Release notes:", "")
|
|
497
|
+
|
|
498
|
+
// Check Playwright availability once for preview images
|
|
499
|
+
const canCapture = await isPlaywrightAvailable()
|
|
500
|
+
|
|
501
|
+
// Preview image (attached to the deck manifest only)
|
|
502
|
+
let previewImage = null
|
|
503
|
+
const previewImagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
|
|
504
|
+
if (previewImagePath) {
|
|
505
|
+
const resolved = join(cwd, previewImagePath)
|
|
506
|
+
previewImage = readPreviewImage(resolved)
|
|
507
|
+
if (!previewImage) {
|
|
508
|
+
console.log(` ${dim("⚠ Skipping image: file not found, unsupported format, or > 2MB")}`)
|
|
509
|
+
}
|
|
510
|
+
} else if (canCapture) {
|
|
511
|
+
const firstSlide = deckConfig.slides[0]
|
|
512
|
+
if (firstSlide) {
|
|
513
|
+
console.log(` ${dim("Generating deck preview image...")}`)
|
|
514
|
+
previewImage = await captureSlideAsDataUri({ cwd, slidePath: `src/slides/${firstSlide.slug}.tsx` })
|
|
515
|
+
if (previewImage) {
|
|
516
|
+
console.log(` ${green("✓")} Deck preview image generated`)
|
|
517
|
+
} else {
|
|
518
|
+
console.log(` ${dim("⚠ Could not generate deck preview image")}`)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
const pm = detectPackageManager(cwd)
|
|
523
|
+
const installCmd = pm === "bun" ? "bun add -d playwright" : pm === "pnpm" ? "pnpm add -D playwright" : pm === "yarn" ? "yarn add -D playwright" : "npm install -D playwright"
|
|
524
|
+
console.log(` ${dim("Tip: Install playwright for auto-generated preview images")}`)
|
|
525
|
+
console.log(` ${dim(` ${installCmd} && npx playwright install chromium`)}`)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
console.log()
|
|
529
|
+
console.log(` Publishing ${totalItems} items...`)
|
|
530
|
+
console.log()
|
|
531
|
+
|
|
532
|
+
// Read lockfile for skip-if-unchanged
|
|
533
|
+
const lock = readLockfile(cwd)
|
|
534
|
+
let itemIndex = 0
|
|
535
|
+
let published = 0
|
|
536
|
+
let skipped = 0
|
|
537
|
+
let failed = 0
|
|
538
|
+
|
|
539
|
+
function isUnchanged(slug, fileHashes) {
|
|
540
|
+
const entry = lock.items[slug]
|
|
541
|
+
if (!entry?.files) return false
|
|
542
|
+
const storedKeys = Object.keys(entry.files)
|
|
543
|
+
const currentKeys = Object.keys(fileHashes)
|
|
544
|
+
if (storedKeys.length !== currentKeys.length) return false
|
|
545
|
+
return currentKeys.every(k => entry.files[k] === fileHashes[k])
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── Phase 1: Assets ──
|
|
549
|
+
for (const asset of publicAssets) {
|
|
550
|
+
itemIndex++
|
|
551
|
+
const assetSlug = assetFileToSlug(deckPrefix, asset.relativePath)
|
|
552
|
+
const fileName = basename(asset.fullPath)
|
|
553
|
+
const dirPart = asset.relativePath.includes("/")
|
|
554
|
+
? "public/" + asset.relativePath.substring(0, asset.relativePath.lastIndexOf("/") + 1)
|
|
555
|
+
: "public/"
|
|
556
|
+
|
|
557
|
+
const fileData = readFileForRegistry(asset.fullPath)
|
|
558
|
+
const fileHash = fileData.binary
|
|
559
|
+
? hashContent(fileData.buffer.toString("base64"))
|
|
560
|
+
: hashContent(fileData.content)
|
|
561
|
+
const fileHashes = { [dirPart + fileName]: fileHash }
|
|
562
|
+
|
|
563
|
+
if (isUnchanged(assetSlug, fileHashes)) {
|
|
564
|
+
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} asset ${dim(assetSlug)} (unchanged)`)
|
|
565
|
+
skipped++
|
|
566
|
+
continue
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let files
|
|
570
|
+
if (fileData.binary) {
|
|
571
|
+
files = await uploadBinaryFiles(assetSlug, [
|
|
572
|
+
{ path: fileName, target: dirPart, binary: true, buffer: fileData.buffer, contentType: fileData.contentType, size: fileData.size }
|
|
573
|
+
], auth)
|
|
574
|
+
} else {
|
|
575
|
+
files = [{ path: fileName, target: dirPart, content: fileData.content }]
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const result = await publishToRegistry({ type: "asset", slug: assetSlug, title: fileName, files }, auth)
|
|
580
|
+
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} asset ${cyan(assetSlug)} ${dim(`v${result.version}`)}`)
|
|
581
|
+
updateLockfileItem(cwd, assetSlug, result.version ?? 0, fileHashes)
|
|
582
|
+
published++
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} asset ${dim(assetSlug)}: ${err.message}`)
|
|
585
|
+
failed++
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ── Phase 2: Theme ──
|
|
590
|
+
if (hasTheme) {
|
|
591
|
+
itemIndex++
|
|
592
|
+
const themeSlug = `${deckPrefix}/theme`
|
|
593
|
+
const themePayloadFiles = []
|
|
594
|
+
const themeFileHashes = {}
|
|
595
|
+
let themeContent = ""
|
|
596
|
+
|
|
597
|
+
for (const tf of themeFilePaths) {
|
|
598
|
+
const content = readFileSync(join(cwd, "src", tf), "utf-8")
|
|
599
|
+
themePayloadFiles.push({ path: tf, target: "src/", content })
|
|
600
|
+
themeFileHashes[`src/${tf}`] = hashContent(content)
|
|
601
|
+
themeContent += content
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (isUnchanged(themeSlug, themeFileHashes)) {
|
|
605
|
+
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} theme ${dim(themeSlug)} (unchanged)`)
|
|
606
|
+
skipped++
|
|
607
|
+
} else {
|
|
608
|
+
const assetDeps = detectAssetDepsInContent(themeContent, deckPrefix, publicFileSet)
|
|
609
|
+
const npmDeps = detectNpmDeps(themeContent)
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const result = await publishToRegistry({
|
|
613
|
+
type: "theme",
|
|
614
|
+
slug: themeSlug,
|
|
615
|
+
title: "Theme",
|
|
616
|
+
files: themePayloadFiles,
|
|
617
|
+
registryDependencies: assetDeps.length ? assetDeps : undefined,
|
|
618
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined
|
|
619
|
+
}, auth)
|
|
620
|
+
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} theme ${cyan(themeSlug)} ${dim(`v${result.version}`)}`)
|
|
621
|
+
updateLockfileItem(cwd, themeSlug, result.version ?? 0, themeFileHashes)
|
|
622
|
+
published++
|
|
623
|
+
} catch (err) {
|
|
624
|
+
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} theme ${dim(themeSlug)}: ${err.message}`)
|
|
625
|
+
failed++
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── Phase 3: Layouts ──
|
|
631
|
+
for (const layoutFile of layoutEntries) {
|
|
632
|
+
itemIndex++
|
|
633
|
+
const layoutName = layoutFile.replace(/\.tsx?$/, "")
|
|
634
|
+
const layoutSlug = `${deckPrefix}/${layoutName}`
|
|
635
|
+
const content = readFileSync(join(cwd, "src", "layouts", layoutFile), "utf-8")
|
|
636
|
+
const fileHashes = { [`src/layouts/${layoutFile}`]: hashContent(content) }
|
|
637
|
+
|
|
638
|
+
if (isUnchanged(layoutSlug, fileHashes)) {
|
|
639
|
+
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} layout ${dim(layoutSlug)} (unchanged)`)
|
|
640
|
+
skipped++
|
|
641
|
+
continue
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const assetDeps = detectAssetDepsInContent(content, deckPrefix, publicFileSet)
|
|
645
|
+
const npmDeps = detectNpmDeps(content)
|
|
646
|
+
const regDeps = hasTheme ? [`${deckPrefix}/theme`] : []
|
|
647
|
+
regDeps.push(...assetDeps)
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const result = await publishToRegistry({
|
|
651
|
+
type: "layout",
|
|
652
|
+
slug: layoutSlug,
|
|
653
|
+
title: titleCase(layoutName),
|
|
654
|
+
files: [{ path: layoutFile, target: "src/layouts/", content }],
|
|
655
|
+
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
656
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined
|
|
657
|
+
}, auth)
|
|
658
|
+
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} layout ${cyan(layoutSlug)} ${dim(`v${result.version}`)}`)
|
|
659
|
+
updateLockfileItem(cwd, layoutSlug, result.version ?? 0, fileHashes)
|
|
660
|
+
published++
|
|
661
|
+
} catch (err) {
|
|
662
|
+
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} layout ${dim(layoutSlug)}: ${err.message}`)
|
|
663
|
+
failed++
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── Phase 4: Slides ──
|
|
668
|
+
for (const slideFile of slideEntries) {
|
|
669
|
+
itemIndex++
|
|
670
|
+
const slideName = slideFile.replace(/\.tsx?$/, "")
|
|
671
|
+
const slideSlug = `${deckPrefix}/${slideName}`
|
|
672
|
+
const content = readFileSync(join(cwd, "src", "slides", slideFile), "utf-8")
|
|
673
|
+
const fileHashes = { [`src/slides/${slideFile}`]: hashContent(content) }
|
|
674
|
+
|
|
675
|
+
if (isUnchanged(slideSlug, fileHashes)) {
|
|
676
|
+
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} slide ${dim(slideSlug)} (unchanged)`)
|
|
677
|
+
skipped++
|
|
678
|
+
continue
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const assetDeps = detectAssetDepsInContent(content, deckPrefix, publicFileSet)
|
|
682
|
+
const layoutDeps = detectRegistryDeps(content).map(d => `${deckPrefix}/${d}`)
|
|
683
|
+
const npmDeps = detectNpmDeps(content)
|
|
684
|
+
const steps = detectSteps(content)
|
|
685
|
+
const slideConfig = deckConfig.slides.find(s => s.slug === slideName)
|
|
686
|
+
const section = slideConfig?.section || undefined
|
|
687
|
+
|
|
688
|
+
const regDeps = hasTheme ? [`${deckPrefix}/theme`] : []
|
|
689
|
+
regDeps.push(...layoutDeps, ...assetDeps)
|
|
690
|
+
|
|
691
|
+
// Generate preview image for this slide
|
|
692
|
+
let slidePreview = null
|
|
693
|
+
if (canCapture) {
|
|
694
|
+
slidePreview = await captureSlideAsDataUri({ cwd, slidePath: `src/slides/${slideFile}` }).catch(() => null)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
const result = await publishToRegistry({
|
|
699
|
+
type: "slide",
|
|
700
|
+
slug: slideSlug,
|
|
701
|
+
title: titleCase(slideName),
|
|
702
|
+
files: [{ path: slideFile, target: "src/slides/", content }],
|
|
703
|
+
steps,
|
|
704
|
+
section,
|
|
705
|
+
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
706
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
707
|
+
previewImage: slidePreview || undefined
|
|
708
|
+
}, auth)
|
|
709
|
+
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} slide ${cyan(slideSlug)} ${dim(`v${result.version}`)}`)
|
|
710
|
+
updateLockfileItem(cwd, slideSlug, result.version ?? 0, fileHashes)
|
|
711
|
+
published++
|
|
712
|
+
} catch (err) {
|
|
713
|
+
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} slide ${dim(slideSlug)}: ${err.message}`)
|
|
714
|
+
failed++
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── Phase 5: Deck manifest ──
|
|
719
|
+
itemIndex++
|
|
720
|
+
const slideSlugs = slideEntries.map(f => `${deckPrefix}/${f.replace(/\.tsx?$/, "")}`)
|
|
721
|
+
const assetSlugs = publicAssets.map(a => assetFileToSlug(deckPrefix, a.relativePath))
|
|
722
|
+
const allDeckDeps = [...slideSlugs, ...assetSlugs]
|
|
723
|
+
|
|
724
|
+
try {
|
|
725
|
+
const result = await publishToRegistry({
|
|
726
|
+
type: "deck",
|
|
727
|
+
slug: deckSlug,
|
|
728
|
+
title,
|
|
729
|
+
description: description || undefined,
|
|
730
|
+
tags,
|
|
731
|
+
deckConfig,
|
|
732
|
+
files: [],
|
|
733
|
+
registryDependencies: allDeckDeps.length ? allDeckDeps : undefined,
|
|
734
|
+
releaseNotes: releaseNotes || undefined,
|
|
735
|
+
previewImage: previewImage || undefined
|
|
736
|
+
}, auth)
|
|
737
|
+
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} deck ${cyan(deckSlug)} ${dim(`v${result.version}`)}`)
|
|
738
|
+
updateLockfileItem(cwd, deckSlug, result.version ?? 0, {})
|
|
739
|
+
published++
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} deck ${dim(deckSlug)}: ${err.message}`)
|
|
742
|
+
failed++
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
console.log()
|
|
746
|
+
console.log(` ${bold("Done:")} ${green(`${published} published`)}${skipped ? `, ${skipped} unchanged` : ""}${failed ? `, ${red(`${failed} failed`)}` : ""}`)
|
|
747
|
+
console.log(` Install: ${cyan(`promptslide add ${deckSlug}`)}`)
|
|
748
|
+
console.log()
|
|
749
|
+
closePrompts()
|
|
750
|
+
return
|
|
225
751
|
}
|
|
226
752
|
|
|
227
753
|
// Resolve full path and read content
|
|
@@ -233,7 +759,7 @@ export async function publish(args) {
|
|
|
233
759
|
|
|
234
760
|
const content = readFileSync(fullPath, "utf-8")
|
|
235
761
|
const fileName = basename(fullPath)
|
|
236
|
-
const
|
|
762
|
+
const baseSlug = fileName.replace(/\.tsx?$/, "")
|
|
237
763
|
|
|
238
764
|
// Detect metadata
|
|
239
765
|
const type = typeOverride || detectType(filePath) || "slide"
|
|
@@ -253,12 +779,34 @@ export async function publish(args) {
|
|
|
253
779
|
}
|
|
254
780
|
console.log()
|
|
255
781
|
|
|
782
|
+
// Prompt for deck prefix (required)
|
|
783
|
+
const deckPrefix = await promptDeckPrefix(cwd, true)
|
|
784
|
+
const slug = `${deckPrefix}/${baseSlug}`
|
|
785
|
+
console.log(` Slug: ${cyan(slug)}`)
|
|
786
|
+
console.log()
|
|
787
|
+
|
|
788
|
+
// Warn if same base slug exists under a different prefix
|
|
789
|
+
try {
|
|
790
|
+
const { items: results } = await searchRegistry({ search: baseSlug, type }, auth)
|
|
791
|
+
const conflicts = (results || []).filter(r => r.name !== slug && r.name.endsWith(`/${baseSlug}`))
|
|
792
|
+
if (conflicts.length) {
|
|
793
|
+
console.log(` ${bold("Note:")} ${baseSlug} also exists as:`)
|
|
794
|
+
for (const c of conflicts) {
|
|
795
|
+
console.log(` ${dim("·")} ${c.name} ${dim(`(${c.title || "untitled"})`)}`)
|
|
796
|
+
}
|
|
797
|
+
console.log()
|
|
798
|
+
}
|
|
799
|
+
} catch {
|
|
800
|
+
// Search failure is non-blocking
|
|
801
|
+
}
|
|
802
|
+
|
|
256
803
|
// Check if registry dependencies exist and offer to publish missing ones
|
|
257
804
|
if (registryDeps.length) {
|
|
258
805
|
const missing = []
|
|
259
806
|
|
|
260
807
|
for (const depSlug of registryDeps) {
|
|
261
|
-
const
|
|
808
|
+
const prefixedDepSlug = `${deckPrefix}/${depSlug}`
|
|
809
|
+
const exists = await registryItemExists(prefixedDepSlug, auth)
|
|
262
810
|
if (!exists) {
|
|
263
811
|
missing.push(depSlug)
|
|
264
812
|
}
|
|
@@ -294,7 +842,8 @@ export async function publish(args) {
|
|
|
294
842
|
cwd,
|
|
295
843
|
auth,
|
|
296
844
|
typeOverride: depType,
|
|
297
|
-
interactive: true
|
|
845
|
+
interactive: true,
|
|
846
|
+
deckPrefix
|
|
298
847
|
})
|
|
299
848
|
console.log()
|
|
300
849
|
const depVer = result.version ? ` ${dim(`v${result.version}`)}` : ""
|
|
@@ -311,13 +860,13 @@ export async function publish(args) {
|
|
|
311
860
|
}
|
|
312
861
|
}
|
|
313
862
|
|
|
314
|
-
console.log(` ${dim("─── Main item:")} ${bold(
|
|
863
|
+
console.log(` ${dim("─── Main item:")} ${bold(baseSlug)} ${dim("───")}`)
|
|
315
864
|
console.log()
|
|
316
865
|
}
|
|
317
866
|
}
|
|
318
867
|
|
|
319
868
|
// Collect metadata for main item
|
|
320
|
-
const title = await prompt("Title:", titleCase(
|
|
869
|
+
const title = await prompt("Title:", titleCase(baseSlug))
|
|
321
870
|
const description = await prompt("Description:", "")
|
|
322
871
|
const tagsInput = await prompt("Tags (comma-separated):", "")
|
|
323
872
|
const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
@@ -349,6 +898,7 @@ export async function publish(args) {
|
|
|
349
898
|
console.log()
|
|
350
899
|
|
|
351
900
|
// Publish main item
|
|
901
|
+
const prefixedRegistryDeps = registryDeps.map(d => `${deckPrefix}/${d}`)
|
|
352
902
|
const payload = {
|
|
353
903
|
type,
|
|
354
904
|
slug,
|
|
@@ -359,7 +909,7 @@ export async function publish(args) {
|
|
|
359
909
|
section: section || undefined,
|
|
360
910
|
files: [{ path: fileName, target, content }],
|
|
361
911
|
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
362
|
-
registryDependencies:
|
|
912
|
+
registryDependencies: prefixedRegistryDeps.length ? prefixedRegistryDeps : undefined,
|
|
363
913
|
releaseNotes: releaseNotes || undefined,
|
|
364
914
|
previewImage: previewImage || undefined
|
|
365
915
|
}
|
package/src/utils/auth.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from "node:os"
|
|
|
5
5
|
const AUTH_DIR = join(homedir(), ".promptslide")
|
|
6
6
|
const AUTH_FILE = join(AUTH_DIR, "auth.json")
|
|
7
7
|
|
|
8
|
-
const DEFAULT_REGISTRY = "https://
|
|
8
|
+
const DEFAULT_REGISTRY = "https://promptslide.eu"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Load stored auth credentials.
|
|
@@ -206,3 +206,58 @@ export function replaceDeckConfig(cwd, slides, opts = {}) {
|
|
|
206
206
|
writeFileSync(configPath, content, "utf-8")
|
|
207
207
|
return true
|
|
208
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse deck-config.ts and extract the deckConfig structure for publishing.
|
|
212
|
+
* Returns { transition?, directionalTransition?, slides } where slides is
|
|
213
|
+
* an array of { slug, steps, section? }.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} cwd - Project root directory
|
|
216
|
+
* @returns {{ transition?: string, directionalTransition?: boolean, slides: { slug: string, steps: number, section?: string }[] } | null}
|
|
217
|
+
*/
|
|
218
|
+
export function parseDeckConfig(cwd) {
|
|
219
|
+
const configPath = join(cwd, DECK_CONFIG_PATH)
|
|
220
|
+
if (!existsSync(configPath)) return null
|
|
221
|
+
|
|
222
|
+
const content = readFileSync(configPath, "utf-8")
|
|
223
|
+
|
|
224
|
+
// 1. Parse imports to build componentName -> slug map
|
|
225
|
+
// Pattern: import { SlideTitle } from "@/slides/slide-title"
|
|
226
|
+
const componentToSlug = {}
|
|
227
|
+
const importRegex = /import\s*\{\s*(\w+)\s*\}\s*from\s*["']@\/slides\/([^"']+)["']/g
|
|
228
|
+
for (const match of content.matchAll(importRegex)) {
|
|
229
|
+
componentToSlug[match[1]] = match[2].replace(/\.tsx?$/, "")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 2. Parse transition exports
|
|
233
|
+
let transition
|
|
234
|
+
const transitionMatch = content.match(/export\s+const\s+transition\s*=\s*["']([^"']+)["']/)
|
|
235
|
+
if (transitionMatch) transition = transitionMatch[1]
|
|
236
|
+
|
|
237
|
+
let directionalTransition
|
|
238
|
+
const dirMatch = content.match(/export\s+const\s+directionalTransition\s*=\s*(true|false)/)
|
|
239
|
+
if (dirMatch) directionalTransition = dirMatch[1] === "true"
|
|
240
|
+
|
|
241
|
+
// 3. Parse slides array entries (order-independent matching)
|
|
242
|
+
const slides = []
|
|
243
|
+
const entryRegex = /\{([^}]+)\}/g
|
|
244
|
+
for (const match of content.matchAll(entryRegex)) {
|
|
245
|
+
const body = match[1]
|
|
246
|
+
const componentMatch = body.match(/component:\s*(\w+)/)
|
|
247
|
+
const stepsMatch = body.match(/steps:\s*(\d+)/)
|
|
248
|
+
if (!componentMatch || !stepsMatch) continue
|
|
249
|
+
const slug = componentToSlug[componentMatch[1]]
|
|
250
|
+
if (!slug) continue // layout or unknown import — skip
|
|
251
|
+
const entry = { slug, steps: parseInt(stepsMatch[1], 10) }
|
|
252
|
+
const sectionMatch = body.match(/section:\s*["']([^"']+)["']/)
|
|
253
|
+
if (sectionMatch) entry.section = sectionMatch[1]
|
|
254
|
+
slides.push(entry)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (slides.length === 0) return null
|
|
258
|
+
|
|
259
|
+
const result = { slides }
|
|
260
|
+
if (transition) result.transition = transition
|
|
261
|
+
if (directionalTransition !== undefined) result.directionalTransition = directionalTransition
|
|
262
|
+
return result
|
|
263
|
+
}
|
package/src/utils/prompts.mjs
CHANGED
|
@@ -28,10 +28,12 @@ export function prompt(question, defaultValue) {
|
|
|
28
28
|
if (!rl) return Promise.resolve(defaultValue || "")
|
|
29
29
|
return new Promise(resolve => {
|
|
30
30
|
const suffix = defaultValue ? ` ${dim(`(${defaultValue})`)} ` : " "
|
|
31
|
+
const onClose = () => resolve(defaultValue || "")
|
|
32
|
+
rl.once("close", onClose)
|
|
31
33
|
rl.question(` ${question}${suffix}`, answer => {
|
|
34
|
+
rl.removeListener("close", onClose)
|
|
32
35
|
resolve(answer.trim() || defaultValue || "")
|
|
33
36
|
})
|
|
34
|
-
rl.once("close", () => resolve(defaultValue || ""))
|
|
35
37
|
})
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -40,11 +42,13 @@ export function confirm(question, defaultYes = true) {
|
|
|
40
42
|
if (!rl) return Promise.resolve(defaultYes)
|
|
41
43
|
return new Promise(resolve => {
|
|
42
44
|
const hint = defaultYes ? "Y/n" : "y/N"
|
|
45
|
+
const onClose = () => resolve(defaultYes)
|
|
46
|
+
rl.once("close", onClose)
|
|
43
47
|
rl.question(` ${question} ${dim(`(${hint})`)} `, answer => {
|
|
48
|
+
rl.removeListener("close", onClose)
|
|
44
49
|
const a = answer.trim().toLowerCase()
|
|
45
50
|
if (!a) return resolve(defaultYes)
|
|
46
51
|
resolve(a === "y" || a === "yes")
|
|
47
52
|
})
|
|
48
|
-
rl.once("close", () => resolve(defaultYes))
|
|
49
53
|
})
|
|
50
54
|
}
|
package/src/utils/registry.mjs
CHANGED
|
@@ -186,26 +186,23 @@ export async function resolveRegistryDependencies(item, auth, cwd) {
|
|
|
186
186
|
Object.assign(npmDeps, current.dependencies)
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
const targetPath = join(cwd, f.target, f.path)
|
|
192
|
-
return !existsSync(targetPath)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
if (needsInstall || current === item) {
|
|
196
|
-
items.push(current)
|
|
197
|
-
}
|
|
189
|
+
// Always include items — add.mjs handles identical/overwrite logic per file
|
|
190
|
+
items.push(current)
|
|
198
191
|
|
|
199
|
-
//
|
|
192
|
+
// Resolve registry dependencies in parallel
|
|
200
193
|
if (current.registryDependencies?.length) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
const fetches = current.registryDependencies
|
|
195
|
+
.filter(depName => !seen.has(depName))
|
|
196
|
+
.map(async (depName) => {
|
|
197
|
+
try {
|
|
198
|
+
return await fetchRegistryItem(depName, auth)
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.warn(` Warning: Could not resolve dependency "${depName}": ${err.message}`)
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
const depItems = (await Promise.all(fetches)).filter(Boolean)
|
|
205
|
+
await Promise.all(depItems.map(dep => resolve(dep)))
|
|
209
206
|
}
|
|
210
207
|
}
|
|
211
208
|
|
|
@@ -317,3 +314,116 @@ export function getInstallCommand(pm, packages) {
|
|
|
317
314
|
return { cmd: "npm", args: ["install", ...packages], display: `npm install ${packages.join(" ")}` }
|
|
318
315
|
}
|
|
319
316
|
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Request pre-signed upload tokens for binary files.
|
|
320
|
+
* Returns empty array if registry doesn't support direct upload (local dev).
|
|
321
|
+
*
|
|
322
|
+
* @param {string} slug - Item slug
|
|
323
|
+
* @param {{ path: string, contentType: string, size: number }[]} files
|
|
324
|
+
* @param {{ registry: string, token: string }} auth
|
|
325
|
+
* @returns {Promise<{ path: string, clientToken: string, pathname: string }[]>}
|
|
326
|
+
*/
|
|
327
|
+
export async function requestUploadTokens(slug, files, auth) {
|
|
328
|
+
const res = await fetch(`${auth.registry}/api/publish/upload-tokens`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: {
|
|
331
|
+
...authHeaders(auth),
|
|
332
|
+
"Content-Type": "application/json"
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify({ slug, files })
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
if (res.status === 404) {
|
|
338
|
+
// Registry doesn't have this endpoint (older version) — fall back to inline
|
|
339
|
+
return []
|
|
340
|
+
}
|
|
341
|
+
if (res.status === 401) {
|
|
342
|
+
throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
|
|
343
|
+
}
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
throw new Error(`Failed to get upload tokens (${res.status}): ${await res.text()}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const data = await res.json()
|
|
349
|
+
return data.tokens || []
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Upload a binary file directly to Vercel Blob using a scoped client token.
|
|
354
|
+
* Uses raw fetch — no @vercel/blob dependency needed.
|
|
355
|
+
*
|
|
356
|
+
* @param {Buffer} buffer - Raw file bytes
|
|
357
|
+
* @param {string} pathname - Target pathname in blob storage
|
|
358
|
+
* @param {string} contentType - MIME type
|
|
359
|
+
* @param {string} clientToken - Scoped Vercel Blob client token
|
|
360
|
+
* @returns {Promise<string>} The permanent blob URL
|
|
361
|
+
*/
|
|
362
|
+
export async function uploadBinaryToBlob(buffer, pathname, contentType, clientToken) {
|
|
363
|
+
const res = await fetch(`https://blob.vercel-storage.com/${pathname}`, {
|
|
364
|
+
method: "PUT",
|
|
365
|
+
headers: {
|
|
366
|
+
Authorization: `Bearer ${clientToken}`,
|
|
367
|
+
"x-content-type": contentType,
|
|
368
|
+
"x-api-version": "7",
|
|
369
|
+
"x-vercel-blob-access": "private"
|
|
370
|
+
},
|
|
371
|
+
body: buffer
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
const text = await res.text()
|
|
376
|
+
throw new Error(`Blob upload failed (${res.status}): ${text}`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const result = await res.json()
|
|
380
|
+
return result.url
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Convert a public/ file path to a registry slug.
|
|
385
|
+
* Deterministic mapping used both during publish (to create the slug)
|
|
386
|
+
* and during asset reference detection (to find the right dependency slug).
|
|
387
|
+
*
|
|
388
|
+
* "logo.png" → "{prefix}/logo-png"
|
|
389
|
+
* "images/hero.jpg" → "{prefix}/images-hero-jpg"
|
|
390
|
+
*
|
|
391
|
+
* @param {string} prefix - Deck prefix (e.g. "my-deck")
|
|
392
|
+
* @param {string} relativePath - Path relative to public/ (e.g. "images/hero.jpg")
|
|
393
|
+
* @returns {string} Full registry slug
|
|
394
|
+
*/
|
|
395
|
+
export function assetFileToSlug(prefix, relativePath) {
|
|
396
|
+
const segment = relativePath
|
|
397
|
+
.replace(/[/\\]/g, "-")
|
|
398
|
+
.replace(/[^a-z0-9-]/gi, "-")
|
|
399
|
+
.toLowerCase()
|
|
400
|
+
.replace(/-+/g, "-")
|
|
401
|
+
.replace(/^-|-$/g, "")
|
|
402
|
+
return `${prefix}/${segment}`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Scan component source for references to public/ assets.
|
|
407
|
+
* Detects patterns like src="/file.png", href="/file.woff2", url('/file.jpg').
|
|
408
|
+
*
|
|
409
|
+
* @param {string} content - File source code (.tsx, .ts, .css)
|
|
410
|
+
* @param {string} prefix - Registry prefix (e.g. "my-deck")
|
|
411
|
+
* @param {Set<string>} publicFiles - Set of known public/ file paths (relative to public/)
|
|
412
|
+
* @returns {string[]} Array of registry dependency slugs
|
|
413
|
+
*/
|
|
414
|
+
export function detectAssetDepsInContent(content, prefix, publicFiles) {
|
|
415
|
+
const deps = new Set()
|
|
416
|
+
const patterns = [
|
|
417
|
+
/(?:src|href|poster|data-src)\s*=\s*\{?\s*["']\/([^"']+)["']/g,
|
|
418
|
+
/url\(\s*["']?\/([^"')]+)["']?\s*\)/g,
|
|
419
|
+
]
|
|
420
|
+
for (const pattern of patterns) {
|
|
421
|
+
for (const match of content.matchAll(pattern)) {
|
|
422
|
+
const filePath = match[1]
|
|
423
|
+
if (publicFiles.has(filePath)) {
|
|
424
|
+
deps.add(assetFileToSlug(prefix, filePath))
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return Array.from(deps)
|
|
429
|
+
}
|
|
@@ -4,6 +4,12 @@ Slide deck framework: Vite + React 19 + Tailwind v4 + Framer Motion. Each slide
|
|
|
4
4
|
|
|
5
5
|
> **Recommended**: Install the [PromptSlide skill](https://github.com/prompticeu/promptslide) for guided slide authoring, style presets, design recipes, and best practices: `npx skills add prompticeu/promptslide`
|
|
6
6
|
|
|
7
|
+
## Before You Start
|
|
8
|
+
|
|
9
|
+
Do not jump straight to writing slides. First confirm the visual direction with the user — theme colors, fonts, and overall style. Then, for each slide, think about what design approach fits the content before coding. Not everything needs cards or grids — let the content shape the layout.
|
|
10
|
+
|
|
11
|
+
If the PromptSlide skill is installed, follow its workflow — it includes design planning steps that should happen before writing any slides.
|
|
12
|
+
|
|
7
13
|
---
|
|
8
14
|
|
|
9
15
|
## Architecture
|
|
@@ -83,5 +89,5 @@ Layouts in `src/layouts/` and theme colors in `src/globals.css` are yours to cus
|
|
|
83
89
|
- **Semantic colors**: Use `text-foreground`, `text-muted-foreground`, `text-primary`, `bg-background`, `bg-card`, `border-border`.
|
|
84
90
|
- **Icons**: Import from `lucide-react` (e.g., `import { ArrowRight } from "lucide-react"`).
|
|
85
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`.
|
|
86
|
-
- **PDF compatibility**: No `blur()` or `backdrop-filter` (dropped by Chromium). No gradients (use solid colors with opacity).
|
|
92
|
+
- **PDF compatibility**: No `blur()` or `backdrop-filter` (dropped by Chromium). No gradients (use solid colors with opacity). No `box-shadow` (doesn't export correctly) — use borders or background tints instead.
|
|
87
93
|
- **Brand color**: Edit `--primary` in `src/globals.css`. Configure logo and fonts in `src/theme.ts`.
|
|
File without changes
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
|
2
|
-
<rect width="32" height="32" rx="6" fill="currentColor" opacity="0.1"/>
|
|
3
|
-
<rect x="6" y="8" width="20" height="14" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
4
|
-
<line x1="6" y1="25" x2="26" y2="25" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
5
|
-
<rect x="10" y="12" width="8" height="2" rx="1" fill="currentColor" opacity="0.6"/>
|
|
6
|
-
<rect x="10" y="16" width="12" height="1.5" rx="0.75" fill="currentColor" opacity="0.3"/>
|
|
7
|
-
</svg>
|