promptslide 0.3.6 → 0.3.8
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 +17 -0
- package/dist/index.d.ts +80 -2
- package/dist/index.js +700 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/create.mjs +41 -5
- package/src/commands/publish.mjs +74 -6
- package/src/commands/pull.mjs +29 -0
- package/src/core/annotations/adapters/http.ts +43 -0
- package/src/core/annotations/annotation-form.tsx +82 -0
- package/src/core/annotations/annotation-overlay.tsx +198 -0
- package/src/core/annotations/annotation-panel.tsx +150 -0
- package/src/core/annotations/annotation-pin.tsx +42 -0
- package/src/core/annotations/api.ts +23 -0
- package/src/core/annotations/index.ts +5 -0
- package/src/core/annotations/selectors.ts +76 -0
- package/src/core/annotations/types.ts +47 -0
- package/src/core/annotations/use-annotations.ts +50 -0
- package/src/core/index.ts +4 -0
- package/src/core/slide-deck.tsx +168 -83
- package/src/vite/plugin.mjs +54 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Annotation, AnnotationsFile } from "./types"
|
|
2
|
+
|
|
3
|
+
const ENDPOINT = "/__promptslide_annotations"
|
|
4
|
+
|
|
5
|
+
export async function fetchAnnotations(): Promise<Annotation[]> {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(ENDPOINT)
|
|
8
|
+
if (!res.ok) return []
|
|
9
|
+
const data: AnnotationsFile = await res.json()
|
|
10
|
+
return data.annotations || []
|
|
11
|
+
} catch {
|
|
12
|
+
return []
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function saveAnnotations(annotations: Annotation[]): Promise<void> {
|
|
17
|
+
const data: AnnotationsFile = { version: 1, annotations }
|
|
18
|
+
await fetch(ENDPOINT, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify(data)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AnnotationOverlay } from "./annotation-overlay"
|
|
2
|
+
export { AnnotationPanel } from "./annotation-panel"
|
|
3
|
+
export { useAnnotations } from "./use-annotations"
|
|
4
|
+
export { createHttpAdapter } from "./adapters/http"
|
|
5
|
+
export type { Annotation, AnnotationsFile, AnnotationStorageAdapter, AnnotationTarget } from "./types"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AnnotationTarget } from "./types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a composite target descriptor from a clicked DOM element.
|
|
5
|
+
*/
|
|
6
|
+
export function buildElementTarget(
|
|
7
|
+
element: HTMLElement,
|
|
8
|
+
slideRoot: HTMLElement
|
|
9
|
+
): AnnotationTarget {
|
|
10
|
+
const rect = element.getBoundingClientRect()
|
|
11
|
+
const rootRect = slideRoot.getBoundingClientRect()
|
|
12
|
+
|
|
13
|
+
const xPercent = ((rect.left + rect.width / 2 - rootRect.left) / rootRect.width) * 100
|
|
14
|
+
const yPercent = ((rect.top + rect.height / 2 - rootRect.top) / rootRect.height) * 100
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
dataAnnotate: element.getAttribute("data-annotate") || undefined,
|
|
18
|
+
contentNearPin: getTextFingerprint(element),
|
|
19
|
+
position: { xPercent, yPercent }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a stored target back to a DOM element, trying strategies in priority order.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveTarget(
|
|
27
|
+
target: AnnotationTarget,
|
|
28
|
+
slideRoot: HTMLElement
|
|
29
|
+
): { element: HTMLElement | null; method: "dataAnnotate" | "contentNearPin" | "position" } {
|
|
30
|
+
// 1. Try data-annotate attribute
|
|
31
|
+
if (target.dataAnnotate) {
|
|
32
|
+
const el = slideRoot.querySelector<HTMLElement>(`[data-annotate="${target.dataAnnotate}"]`)
|
|
33
|
+
if (el) return { element: el, method: "dataAnnotate" }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Try text content match
|
|
37
|
+
if (target.contentNearPin) {
|
|
38
|
+
const match = findByTextContent(slideRoot, target.contentNearPin)
|
|
39
|
+
if (match) return { element: match, method: "contentNearPin" }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Fallback to coordinates (no element found)
|
|
43
|
+
return { element: null, method: "position" }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract a text fingerprint from an element (first 100 chars, trimmed).
|
|
48
|
+
*/
|
|
49
|
+
function getTextFingerprint(element: HTMLElement): string | undefined {
|
|
50
|
+
const text = element.textContent?.trim()
|
|
51
|
+
if (!text) return undefined
|
|
52
|
+
return text.slice(0, 100)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find an element by matching its text content.
|
|
57
|
+
*/
|
|
58
|
+
function findByTextContent(root: HTMLElement, text: string): HTMLElement | null {
|
|
59
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)
|
|
60
|
+
let best: HTMLElement | null = null
|
|
61
|
+
let bestLength = Infinity
|
|
62
|
+
|
|
63
|
+
while (walker.nextNode()) {
|
|
64
|
+
const el = walker.currentNode as HTMLElement
|
|
65
|
+
const elText = el.textContent?.trim()
|
|
66
|
+
if (!elText) continue
|
|
67
|
+
|
|
68
|
+
const elFingerprint = elText.slice(0, 100)
|
|
69
|
+
if (elFingerprint === text && elText.length < bestLength) {
|
|
70
|
+
best = el
|
|
71
|
+
bestLength = elText.length
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return best
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the slide annotation system.
|
|
3
|
+
* Annotations are stored as a JSON file in the project root
|
|
4
|
+
* and read by coding agents to act on user feedback.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface AnnotationTarget {
|
|
8
|
+
/** User-assigned data-annotate value, if present on the element */
|
|
9
|
+
dataAnnotate?: string
|
|
10
|
+
/** Nearby visible text to help locate the annotated area (first 100 chars, not meant to be edited) */
|
|
11
|
+
contentNearPin?: string
|
|
12
|
+
/** Coordinates as percentage of slide dimensions */
|
|
13
|
+
position: { xPercent: number; yPercent: number }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Annotation {
|
|
17
|
+
/** Unique identifier */
|
|
18
|
+
id: string
|
|
19
|
+
/** Slide index (0-based, matching deck-config order) */
|
|
20
|
+
slideIndex: number
|
|
21
|
+
/** Slide title from SlideConfig (informational, for agent readability) */
|
|
22
|
+
slideTitle: string
|
|
23
|
+
/** Target element identification */
|
|
24
|
+
target: AnnotationTarget
|
|
25
|
+
/** The user's feedback text */
|
|
26
|
+
body: string
|
|
27
|
+
/** ISO 8601 timestamp */
|
|
28
|
+
createdAt: string
|
|
29
|
+
/** open = needs attention, resolved = agent addressed it */
|
|
30
|
+
status: "open" | "resolved"
|
|
31
|
+
/** Agent's note when resolving */
|
|
32
|
+
resolution?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AnnotationsFile {
|
|
36
|
+
version: 1
|
|
37
|
+
annotations: Annotation[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Storage adapter for annotation persistence */
|
|
41
|
+
export interface AnnotationStorageAdapter {
|
|
42
|
+
load(): Promise<Annotation[]>
|
|
43
|
+
add(annotation: Annotation): Promise<void>
|
|
44
|
+
remove(id: string): Promise<void>
|
|
45
|
+
/** Optional: subscribe to external state updates (e.g. postMessage from parent) */
|
|
46
|
+
subscribe?(onUpdate: (annotations: Annotation[]) => void): () => void
|
|
47
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import { createHttpAdapter } from "./adapters/http"
|
|
3
|
+
import type { Annotation, AnnotationStorageAdapter, AnnotationTarget } from "./types"
|
|
4
|
+
|
|
5
|
+
export function useAnnotations(adapter?: AnnotationStorageAdapter) {
|
|
6
|
+
const adapterRef = useRef(adapter ?? createHttpAdapter())
|
|
7
|
+
const [annotations, setAnnotations] = useState<Annotation[]>([])
|
|
8
|
+
|
|
9
|
+
// Load on mount and subscribe to external updates
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
adapterRef.current.load().then(setAnnotations)
|
|
12
|
+
return adapterRef.current.subscribe?.(setAnnotations)
|
|
13
|
+
}, [])
|
|
14
|
+
|
|
15
|
+
const addAnnotation = useCallback(
|
|
16
|
+
(slideIndex: number, slideTitle: string, target: AnnotationTarget, body: string) => {
|
|
17
|
+
const annotation: Annotation = {
|
|
18
|
+
id: crypto.randomUUID(),
|
|
19
|
+
slideIndex,
|
|
20
|
+
slideTitle,
|
|
21
|
+
target,
|
|
22
|
+
body,
|
|
23
|
+
createdAt: new Date().toISOString(),
|
|
24
|
+
status: "open"
|
|
25
|
+
}
|
|
26
|
+
setAnnotations(prev => [...prev, annotation])
|
|
27
|
+
adapterRef.current.add(annotation)
|
|
28
|
+
},
|
|
29
|
+
[]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const deleteAnnotation = useCallback((id: string) => {
|
|
33
|
+
setAnnotations(prev => prev.filter(a => a.id !== id))
|
|
34
|
+
adapterRef.current.remove(id)
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const getSlideAnnotations = useCallback(
|
|
38
|
+
(slideIndex: number) => annotations.filter(a => a.slideIndex === slideIndex),
|
|
39
|
+
[annotations]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const openCount = useMemo(() => annotations.filter(a => a.status === "open").length, [annotations])
|
|
43
|
+
|
|
44
|
+
// Allow external state updates (e.g. from postMessage adapter)
|
|
45
|
+
const updateAnnotations = useCallback((updated: Annotation[]) => {
|
|
46
|
+
setAnnotations(updated)
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
return { annotations, addAnnotation, deleteAnnotation, getSlideAnnotations, openCount, updateAnnotations }
|
|
50
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -73,5 +73,9 @@ export { SlideDeck } from "./slide-deck"
|
|
|
73
73
|
// Error Boundary
|
|
74
74
|
export { SlideErrorBoundary } from "./slide-error-boundary"
|
|
75
75
|
|
|
76
|
+
// Annotations
|
|
77
|
+
export { AnnotationOverlay, useAnnotations, createHttpAdapter } from "./annotations"
|
|
78
|
+
export type { Annotation, AnnotationsFile, AnnotationStorageAdapter, AnnotationTarget } from "./annotations"
|
|
79
|
+
|
|
76
80
|
// Utils
|
|
77
81
|
export { cn } from "./utils"
|
package/src/core/slide-deck.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { LayoutGroup } from "framer-motion"
|
|
2
|
-
import { ChevronLeft, ChevronRight, Download, Grid3X3, List, Maximize, Monitor } from "lucide-react"
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react"
|
|
2
|
+
import { ChevronLeft, ChevronRight, Download, Grid3X3, List, Maximize, MessageCircle, Monitor } from "lucide-react"
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
4
4
|
|
|
5
5
|
import type { SlideTransitionType } from "./transitions"
|
|
6
6
|
import type { SlideConfig } from "./types"
|
|
7
7
|
|
|
8
8
|
import { SLIDE_DIMENSIONS } from "./animation-config"
|
|
9
9
|
import { AnimationProvider } from "./animation-context"
|
|
10
|
+
import { AnnotationOverlay, AnnotationPanel, useAnnotations } from "./annotations"
|
|
11
|
+
import type { Annotation, AnnotationTarget } from "./annotations"
|
|
10
12
|
import { SlideErrorBoundary } from "./slide-error-boundary"
|
|
11
13
|
import { SlideRenderer } from "./slide-renderer"
|
|
12
14
|
import { useSlideNavigation } from "./use-slide-navigation"
|
|
@@ -22,6 +24,12 @@ interface SlideDeckProps {
|
|
|
22
24
|
slides: SlideConfig[]
|
|
23
25
|
transition?: SlideTransitionType
|
|
24
26
|
directionalTransition?: boolean
|
|
27
|
+
/** Annotation data to display. When provided (even empty array), annotation UI is enabled. When undefined, annotation UI is hidden. */
|
|
28
|
+
annotations?: Annotation[]
|
|
29
|
+
/** Called when the user creates an annotation */
|
|
30
|
+
onAnnotationAdd?: (slideIndex: number, slideTitle: string, target: AnnotationTarget, body: string) => void
|
|
31
|
+
/** Called when the user deletes an annotation */
|
|
32
|
+
onAnnotationDelete?: (id: string) => void
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
// =============================================================================
|
|
@@ -66,7 +74,7 @@ function SlideExportView({ slides, slideIndex }: { slides: SlideConfig[]; slideI
|
|
|
66
74
|
// COMPONENT
|
|
67
75
|
// =============================================================================
|
|
68
76
|
|
|
69
|
-
export function SlideDeck({ slides, transition, directionalTransition }: SlideDeckProps) {
|
|
77
|
+
export function SlideDeck({ slides, transition, directionalTransition, annotations, onAnnotationAdd, onAnnotationDelete }: SlideDeckProps) {
|
|
70
78
|
// Check for export mode via URL params
|
|
71
79
|
const [exportParams] = useState(() => {
|
|
72
80
|
if (typeof window === "undefined") return null
|
|
@@ -79,10 +87,27 @@ export function SlideDeck({ slides, transition, directionalTransition }: SlideDe
|
|
|
79
87
|
return <SlideExportView slides={slides} slideIndex={exportParams.slideIndex} />
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
// Use internal useAnnotations as fallback when no external annotations prop is provided
|
|
91
|
+
const internal = useAnnotations()
|
|
92
|
+
const isExternallyManaged = annotations !== undefined
|
|
93
|
+
const effectiveAnnotations = isExternallyManaged ? annotations : internal.annotations
|
|
94
|
+
const effectiveAdd = isExternallyManaged ? onAnnotationAdd : internal.addAnnotation
|
|
95
|
+
const effectiveDelete = isExternallyManaged ? onAnnotationDelete : internal.deleteAnnotation
|
|
96
|
+
|
|
97
|
+
const openCount = useMemo(() => effectiveAnnotations.filter(a => a.status === "open").length, [effectiveAnnotations])
|
|
98
|
+
const getSlideAnnotations = useCallback(
|
|
99
|
+
(slideIndex: number) => effectiveAnnotations.filter(a => a.slideIndex === slideIndex),
|
|
100
|
+
[effectiveAnnotations]
|
|
101
|
+
)
|
|
102
|
+
|
|
82
103
|
const [viewMode, setViewMode] = useState<ViewMode>("slide")
|
|
83
104
|
const [isPresentationMode, setIsPresentationMode] = useState(false)
|
|
105
|
+
const [isAnnotationMode, setIsAnnotationMode] = useState(false)
|
|
106
|
+
const [showAnnotationPanel, setShowAnnotationPanel] = useState(false)
|
|
107
|
+
const [selectedAnnotationId, setSelectedAnnotationId] = useState<string | null>(null)
|
|
84
108
|
const [scale, setScale] = useState(1)
|
|
85
109
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
110
|
+
const slideContainerRef = useRef<HTMLDivElement>(null)
|
|
86
111
|
|
|
87
112
|
const {
|
|
88
113
|
currentSlide,
|
|
@@ -162,7 +187,7 @@ export function SlideDeck({ slides, transition, directionalTransition }: SlideDe
|
|
|
162
187
|
return
|
|
163
188
|
}
|
|
164
189
|
|
|
165
|
-
if (viewMode !== "slide") return
|
|
190
|
+
if (viewMode !== "slide" || isAnnotationMode) return
|
|
166
191
|
|
|
167
192
|
if (e.key === "ArrowRight" || e.key === " ") {
|
|
168
193
|
e.preventDefault()
|
|
@@ -175,7 +200,7 @@ export function SlideDeck({ slides, transition, directionalTransition }: SlideDe
|
|
|
175
200
|
|
|
176
201
|
window.addEventListener("keydown", handleKeyDown)
|
|
177
202
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
178
|
-
}, [advance, goBack, viewMode, togglePresentationMode])
|
|
203
|
+
}, [advance, goBack, viewMode, togglePresentationMode, isAnnotationMode])
|
|
179
204
|
|
|
180
205
|
return (
|
|
181
206
|
<div className="min-h-screen w-full bg-neutral-950 text-foreground">
|
|
@@ -204,8 +229,9 @@ export function SlideDeck({ slides, transition, directionalTransition }: SlideDe
|
|
|
204
229
|
{/* Toolbar */}
|
|
205
230
|
<div
|
|
206
231
|
className={cn(
|
|
207
|
-
"fixed top-4
|
|
208
|
-
isPresentationMode && "hidden"
|
|
232
|
+
"fixed top-4 z-50 flex gap-1 rounded-lg border border-neutral-800 bg-neutral-950/90 p-1 backdrop-blur-sm transition-[right] print:hidden",
|
|
233
|
+
isPresentationMode && "hidden",
|
|
234
|
+
isAnnotationMode && showAnnotationPanel ? "right-[19.5rem]" : "right-4"
|
|
209
235
|
)}
|
|
210
236
|
>
|
|
211
237
|
<button
|
|
@@ -241,6 +267,31 @@ export function SlideDeck({ slides, transition, directionalTransition }: SlideDe
|
|
|
241
267
|
|
|
242
268
|
<div className="mx-1 w-px bg-neutral-800" />
|
|
243
269
|
|
|
270
|
+
<button
|
|
271
|
+
onClick={() => {
|
|
272
|
+
setIsAnnotationMode(prev => {
|
|
273
|
+
const next = !prev
|
|
274
|
+
setShowAnnotationPanel(next)
|
|
275
|
+
if (!next) setSelectedAnnotationId(null)
|
|
276
|
+
return next
|
|
277
|
+
})
|
|
278
|
+
}}
|
|
279
|
+
className={cn(
|
|
280
|
+
"relative rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
281
|
+
isAnnotationMode && "bg-[#FF6B35] text-white hover:bg-[#FF7A4A]"
|
|
282
|
+
)}
|
|
283
|
+
title="Annotate slides"
|
|
284
|
+
>
|
|
285
|
+
<MessageCircle className="h-4 w-4" />
|
|
286
|
+
{openCount > 0 && (
|
|
287
|
+
<span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#FF6B35] text-[10px] font-bold text-white">
|
|
288
|
+
{openCount}
|
|
289
|
+
</span>
|
|
290
|
+
)}
|
|
291
|
+
</button>
|
|
292
|
+
|
|
293
|
+
<div className="mx-1 w-px bg-neutral-800" />
|
|
294
|
+
|
|
244
295
|
<button
|
|
245
296
|
onClick={handleExportPdf}
|
|
246
297
|
className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
|
|
@@ -260,88 +311,122 @@ export function SlideDeck({ slides, transition, directionalTransition }: SlideDe
|
|
|
260
311
|
{/* Slide View */}
|
|
261
312
|
{viewMode === "slide" && (
|
|
262
313
|
<div
|
|
263
|
-
ref={containerRef}
|
|
264
|
-
role="presentation"
|
|
265
|
-
tabIndex={isPresentationMode ? 0 : undefined}
|
|
266
314
|
className={cn(
|
|
267
|
-
"flex h-screen w-full
|
|
268
|
-
isPresentationMode ? "bg-black
|
|
315
|
+
"flex h-screen w-full print:hidden",
|
|
316
|
+
isPresentationMode ? "bg-black" : ""
|
|
269
317
|
)}
|
|
270
|
-
onClick={isPresentationMode ? advance : undefined}
|
|
271
|
-
onKeyDown={
|
|
272
|
-
isPresentationMode
|
|
273
|
-
? e => {
|
|
274
|
-
if (e.key === "Enter" || e.key === " ") advance()
|
|
275
|
-
}
|
|
276
|
-
: undefined
|
|
277
|
-
}
|
|
278
318
|
>
|
|
279
|
-
<
|
|
280
|
-
{
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
transform: `scale(${scale})`,
|
|
287
|
-
transformOrigin: "center center"
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
<SlideRenderer
|
|
291
|
-
slides={slides}
|
|
292
|
-
currentSlide={currentSlide}
|
|
293
|
-
animationStep={animationStep}
|
|
294
|
-
totalSteps={totalSteps}
|
|
295
|
-
direction={direction}
|
|
296
|
-
showAllAnimations={showAllAnimations}
|
|
297
|
-
transition={transition}
|
|
298
|
-
directionalTransition={directionalTransition}
|
|
299
|
-
onTransitionComplete={onTransitionComplete}
|
|
300
|
-
/>
|
|
301
|
-
</div>
|
|
302
|
-
) : (
|
|
303
|
-
<div className="relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl">
|
|
304
|
-
<SlideRenderer
|
|
305
|
-
slides={slides}
|
|
306
|
-
currentSlide={currentSlide}
|
|
307
|
-
animationStep={animationStep}
|
|
308
|
-
totalSteps={totalSteps}
|
|
309
|
-
direction={direction}
|
|
310
|
-
showAllAnimations={showAllAnimations}
|
|
311
|
-
transition={transition}
|
|
312
|
-
directionalTransition={directionalTransition}
|
|
313
|
-
onTransitionComplete={onTransitionComplete}
|
|
314
|
-
/>
|
|
315
|
-
</div>
|
|
319
|
+
<div
|
|
320
|
+
ref={containerRef}
|
|
321
|
+
role="presentation"
|
|
322
|
+
tabIndex={isPresentationMode ? 0 : undefined}
|
|
323
|
+
className={cn(
|
|
324
|
+
"flex flex-1 flex-col items-center justify-center overflow-hidden",
|
|
325
|
+
isPresentationMode ? "bg-black p-0" : "p-4 md:p-8"
|
|
316
326
|
)}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
327
|
+
onClick={isPresentationMode ? advance : undefined}
|
|
328
|
+
onKeyDown={
|
|
329
|
+
isPresentationMode
|
|
330
|
+
? e => {
|
|
331
|
+
if (e.key === "Enter" || e.key === " ") advance()
|
|
332
|
+
}
|
|
333
|
+
: undefined
|
|
334
|
+
}
|
|
335
|
+
>
|
|
336
|
+
<LayoutGroup id="slide-deck">
|
|
337
|
+
{isPresentationMode ? (
|
|
338
|
+
<div
|
|
339
|
+
className="pointer-events-none relative overflow-hidden bg-black"
|
|
340
|
+
style={{
|
|
341
|
+
width: SLIDE_DIMENSIONS.width,
|
|
342
|
+
height: SLIDE_DIMENSIONS.height,
|
|
343
|
+
transform: `scale(${scale})`,
|
|
344
|
+
transformOrigin: "center center"
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
<SlideRenderer
|
|
348
|
+
slides={slides}
|
|
349
|
+
currentSlide={currentSlide}
|
|
350
|
+
animationStep={animationStep}
|
|
351
|
+
totalSteps={totalSteps}
|
|
352
|
+
direction={direction}
|
|
353
|
+
showAllAnimations={showAllAnimations}
|
|
354
|
+
transition={transition}
|
|
355
|
+
directionalTransition={directionalTransition}
|
|
356
|
+
onTransitionComplete={onTransitionComplete}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
) : (
|
|
360
|
+
<div ref={slideContainerRef} className="relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl">
|
|
361
|
+
<SlideRenderer
|
|
362
|
+
slides={slides}
|
|
363
|
+
currentSlide={currentSlide}
|
|
364
|
+
animationStep={animationStep}
|
|
365
|
+
totalSteps={totalSteps}
|
|
366
|
+
direction={direction}
|
|
367
|
+
showAllAnimations={showAllAnimations}
|
|
368
|
+
transition={transition}
|
|
369
|
+
directionalTransition={directionalTransition}
|
|
370
|
+
onTransitionComplete={onTransitionComplete}
|
|
371
|
+
/>
|
|
372
|
+
{isAnnotationMode && (
|
|
373
|
+
<AnnotationOverlay
|
|
374
|
+
slides={slides}
|
|
375
|
+
currentSlide={currentSlide}
|
|
376
|
+
slideContainerRef={slideContainerRef}
|
|
377
|
+
selectedId={selectedAnnotationId}
|
|
378
|
+
onSelectId={setSelectedAnnotationId}
|
|
379
|
+
onShowPanel={() => setShowAnnotationPanel(true)}
|
|
380
|
+
slideAnnotations={getSlideAnnotations(currentSlide)}
|
|
381
|
+
addAnnotation={effectiveAdd ?? (() => {})}
|
|
382
|
+
|
|
383
|
+
/>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
</LayoutGroup>
|
|
388
|
+
|
|
389
|
+
{/* Navigation Controls */}
|
|
390
|
+
{!isPresentationMode && (
|
|
391
|
+
<div className="mt-6 flex items-center gap-4">
|
|
392
|
+
<button
|
|
393
|
+
onClick={goBack}
|
|
394
|
+
className="rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white"
|
|
395
|
+
>
|
|
396
|
+
<ChevronLeft className="h-5 w-5" />
|
|
397
|
+
</button>
|
|
398
|
+
<div className="flex min-w-[4rem] flex-col items-center">
|
|
399
|
+
<span className="font-mono text-sm text-neutral-500">
|
|
400
|
+
{currentSlide + 1} / {slides.length}
|
|
335
401
|
</span>
|
|
336
|
-
|
|
402
|
+
{slides[currentSlide]?.title && (
|
|
403
|
+
<span className="mt-0.5 text-xs text-neutral-600">
|
|
404
|
+
{slides[currentSlide].title}
|
|
405
|
+
</span>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
<button
|
|
409
|
+
onClick={advance}
|
|
410
|
+
className="rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white"
|
|
411
|
+
>
|
|
412
|
+
<ChevronRight className="h-5 w-5" />
|
|
413
|
+
</button>
|
|
337
414
|
</div>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
{/* Annotation Panel — beside the slide */}
|
|
419
|
+
{isAnnotationMode && showAnnotationPanel && !isPresentationMode && (
|
|
420
|
+
<AnnotationPanel
|
|
421
|
+
annotations={getSlideAnnotations(currentSlide)}
|
|
422
|
+
selectedId={selectedAnnotationId}
|
|
423
|
+
onSelect={setSelectedAnnotationId}
|
|
424
|
+
onDelete={effectiveDelete ?? (() => {})}
|
|
425
|
+
onClose={() => {
|
|
426
|
+
setShowAnnotationPanel(false)
|
|
427
|
+
setSelectedAnnotationId(null)
|
|
428
|
+
}}
|
|
429
|
+
/>
|
|
345
430
|
)}
|
|
346
431
|
</div>
|
|
347
432
|
)}
|
package/src/vite/plugin.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
1
3
|
import { bold, dim } from "../utils/ansi.mjs"
|
|
2
4
|
|
|
3
5
|
const VIRTUAL_ENTRY_ID = "virtual:promptslide-entry"
|
|
@@ -156,6 +158,16 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
|
156
158
|
name: "promptslide",
|
|
157
159
|
enforce: "pre",
|
|
158
160
|
|
|
161
|
+
config() {
|
|
162
|
+
return {
|
|
163
|
+
server: {
|
|
164
|
+
watch: {
|
|
165
|
+
ignored: ["**/annotations.json"]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
159
171
|
configResolved(config) {
|
|
160
172
|
if (!root) root = config.root
|
|
161
173
|
},
|
|
@@ -190,6 +202,48 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
|
190
202
|
})
|
|
191
203
|
})
|
|
192
204
|
|
|
205
|
+
// Pre-middleware: read annotations
|
|
206
|
+
server.middlewares.use((req, res, next) => {
|
|
207
|
+
if (req.method !== "GET" || req.url !== "/__promptslide_annotations") return next()
|
|
208
|
+
|
|
209
|
+
const filePath = join(root, "annotations.json")
|
|
210
|
+
try {
|
|
211
|
+
if (existsSync(filePath)) {
|
|
212
|
+
const content = readFileSync(filePath, "utf-8")
|
|
213
|
+
res.setHeader("Content-Type", "application/json")
|
|
214
|
+
res.statusCode = 200
|
|
215
|
+
res.end(content)
|
|
216
|
+
} else {
|
|
217
|
+
res.setHeader("Content-Type", "application/json")
|
|
218
|
+
res.statusCode = 200
|
|
219
|
+
res.end(JSON.stringify({ version: 1, annotations: [] }))
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
res.statusCode = 500
|
|
223
|
+
res.end()
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Pre-middleware: write annotations
|
|
228
|
+
server.middlewares.use((req, res, next) => {
|
|
229
|
+
if (req.method !== "POST" || req.url !== "/__promptslide_annotations") return next()
|
|
230
|
+
|
|
231
|
+
let body = ""
|
|
232
|
+
req.on("data", chunk => { body += chunk })
|
|
233
|
+
req.on("end", () => {
|
|
234
|
+
try {
|
|
235
|
+
const data = JSON.parse(body)
|
|
236
|
+
const filePath = join(root, "annotations.json")
|
|
237
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8")
|
|
238
|
+
res.statusCode = 204
|
|
239
|
+
res.end()
|
|
240
|
+
} catch {
|
|
241
|
+
res.statusCode = 400
|
|
242
|
+
res.end()
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
193
247
|
// Pre-middleware: serve /embed route
|
|
194
248
|
server.middlewares.use(async (req, res, next) => {
|
|
195
249
|
const url = new URL(req.url, "http://localhost")
|