promptslide 0.3.16 → 0.3.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "CLI and slide engine for PromptSlide presentations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,10 +5,14 @@ import { fileURLToPath } from "node:url"
5
5
  import { bold, green, cyan, red, dim, yellow } from "../utils/ansi.mjs"
6
6
  import { requireAuth } from "../utils/auth.mjs"
7
7
  import { hexToOklch } from "../utils/colors.mjs"
8
- import { closePrompts } from "../utils/prompts.mjs"
9
- import { fetchRegistryItem, resolveRegistryDependencies, writeLockfile } from "../utils/registry.mjs"
10
8
  import { toPascalCase, replaceDeckConfig } from "../utils/deck-config.mjs"
11
9
  import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
10
+ import { closePrompts } from "../utils/prompts.mjs"
11
+ import {
12
+ fetchRegistryItem,
13
+ resolveRegistryDependencies,
14
+ writeLockfile
15
+ } from "../utils/registry.mjs"
12
16
  import { ensureTsConfig } from "../utils/tsconfig.mjs"
13
17
 
14
18
  const __filename = fileURLToPath(import.meta.url)
@@ -63,7 +67,9 @@ export async function clone(args) {
63
67
  }
64
68
 
65
69
  if (item.type !== "deck") {
66
- console.error(` ${red("Error:")} "${slug}" is a ${item.type}, not a deck. Only decks can be cloned.`)
70
+ console.error(
71
+ ` ${red("Error:")} "${slug}" is a ${item.type}, not a deck. Only decks can be cloned.`
72
+ )
67
73
  process.exit(1)
68
74
  }
69
75
 
@@ -75,8 +81,12 @@ export async function clone(args) {
75
81
  const localParts = CLI_VERSION.match(/^(\d+)\.(\d+)/)
76
82
  if (pubParts && localParts && pubParts[2] !== localParts[2]) {
77
83
  console.log()
78
- console.log(` ${yellow("⚠")} This deck was published with promptslide ${bold(`v${item.promptslideVersion}`)}`)
79
- console.log(` You have ${bold(`v${CLI_VERSION}`)} installed — some slides may need updating.`)
84
+ console.log(
85
+ ` ${yellow("⚠")} This deck was published with promptslide ${bold(`v${item.promptslideVersion}`)}`
86
+ )
87
+ console.log(
88
+ ` You have ${bold(`v${CLI_VERSION}`)} installed — some slides may need updating.`
89
+ )
80
90
  }
81
91
  }
82
92
 
@@ -100,7 +110,11 @@ export async function clone(args) {
100
110
  const replacements = [
101
111
  {
102
112
  path: join(targetDir, "package.json"),
103
- values: { "{{PROJECT_SLUG}}": dirName, "{{PROJECT_NAME}}": projectName, "{{PROMPTSLIDE_VERSION}}": `^${CLI_VERSION}` }
113
+ values: {
114
+ "{{PROJECT_SLUG}}": dirName,
115
+ "{{PROJECT_NAME}}": projectName,
116
+ "{{PROMPTSLIDE_VERSION}}": `^${CLI_VERSION}`
117
+ }
104
118
  },
105
119
  {
106
120
  path: join(targetDir, "src", "theme.ts"),
@@ -149,7 +163,10 @@ export async function clone(args) {
149
163
 
150
164
  const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
151
165
  if (dataUriPrefix) {
152
- writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
166
+ writeFileSync(
167
+ targetPath,
168
+ Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64")
169
+ )
153
170
  } else {
154
171
  writeFileSync(targetPath, file.content, "utf-8")
155
172
  }
@@ -168,6 +185,7 @@ export async function clone(args) {
168
185
  const slides = available.map(s => ({
169
186
  componentName: s.componentName || toPascalCase(s.slug),
170
187
  importPath: `@/slides/${s.slug}`,
188
+ id: s.id,
171
189
  steps: s.steps,
172
190
  section: s.section
173
191
  }))
@@ -175,7 +193,9 @@ export async function clone(args) {
175
193
  transition: item.meta.transition,
176
194
  directionalTransition: item.meta.directionalTransition
177
195
  })
178
- console.log(` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
196
+ console.log(
197
+ ` ${green("✓")} Generated ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`
198
+ )
179
199
  }
180
200
 
181
201
  // 7. Add npm dependencies
@@ -195,24 +215,37 @@ export async function clone(args) {
195
215
  if (item.id) {
196
216
  try {
197
217
  const annotationsRes = await fetch(`${auth.registry}/api/items/${item.id}/annotations`, {
198
- headers: { Authorization: `Bearer ${auth.token}`, ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {}) }
218
+ headers: {
219
+ Authorization: `Bearer ${auth.token}`,
220
+ ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {})
221
+ }
199
222
  })
200
223
  if (annotationsRes.ok) {
201
224
  const data = await annotationsRes.json()
202
225
  const annotations = data.annotations ?? []
203
226
  if (annotations.length > 0) {
204
- const annotationsFile = { version: 1, annotations: annotations.map(a => ({
205
- id: a.id,
206
- slideIndex: a.slideIndex,
207
- slideTitle: a.slideTitle,
208
- target: a.target,
209
- body: a.body,
210
- createdAt: a.createdAt,
211
- status: a.status,
212
- ...(a.resolution ? { resolution: a.resolution } : {})
213
- })) }
214
- writeFileSync(join(targetDir, "annotations.json"), JSON.stringify(annotationsFile, null, 2) + "\n", "utf-8")
215
- console.log(` ${green("✓")} Pulled ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`)
227
+ const annotationsFile = {
228
+ version: 1,
229
+ annotations: annotations.map(a => ({
230
+ id: a.id,
231
+ slideIndex: a.slideIndex,
232
+ ...(a.slideId ? { slideId: a.slideId } : {}),
233
+ slideTitle: a.slideTitle,
234
+ target: a.target,
235
+ body: a.body,
236
+ createdAt: a.createdAt,
237
+ status: a.status,
238
+ ...(a.resolution ? { resolution: a.resolution } : {})
239
+ }))
240
+ }
241
+ writeFileSync(
242
+ join(targetDir, "annotations.json"),
243
+ JSON.stringify(annotationsFile, null, 2) + "\n",
244
+ "utf-8"
245
+ )
246
+ console.log(
247
+ ` ${green("✓")} Pulled ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`
248
+ )
216
249
  }
217
250
  }
218
251
  } catch {
@@ -1,9 +1,11 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
2
1
  import { execFileSync } from "node:child_process"
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
3
3
  import { join, dirname, resolve, sep } from "node:path"
4
4
 
5
5
  import { bold, green, cyan, red, yellow, dim } from "../utils/ansi.mjs"
6
6
  import { requireAuth } from "../utils/auth.mjs"
7
+ import { toPascalCase, addSlideToDeckConfig, replaceDeckConfig } from "../utils/deck-config.mjs"
8
+ import { confirm, closePrompts } from "../utils/prompts.mjs"
7
9
  import {
8
10
  fetchRegistryItem,
9
11
  resolveRegistryDependencies,
@@ -15,13 +17,7 @@ import {
15
17
  hashFile,
16
18
  isFileDirty
17
19
  } from "../utils/registry.mjs"
18
- import {
19
- toPascalCase,
20
- addSlideToDeckConfig,
21
- replaceDeckConfig
22
- } from "../utils/deck-config.mjs"
23
20
  import { collectRegistrySlideSlugs, partitionDeckSlides } from "../utils/deck-sync.mjs"
24
- import { confirm, closePrompts } from "../utils/prompts.mjs"
25
21
 
26
22
  export async function pull(args) {
27
23
  const cwd = process.cwd()
@@ -51,7 +47,9 @@ export async function pull(args) {
51
47
 
52
48
  if (!deckSlug) {
53
49
  console.error(` ${red("Error:")} No deck slug found in lockfile.`)
54
- console.error(` ${dim("Run")} ${cyan("promptslide publish --type deck")} ${dim("first to set it.")}`)
50
+ console.error(
51
+ ` ${dim("Run")} ${cyan("promptslide publish --type deck")} ${dim("first to set it.")}`
52
+ )
55
53
  console.log()
56
54
  process.exit(1)
57
55
  }
@@ -69,7 +67,9 @@ export async function pull(args) {
69
67
  }
70
68
 
71
69
  const versionTag = deckItem.version ? ` ${dim(`v${deckItem.version}`)}` : ""
72
- console.log(` Found ${bold(deckItem.title || deckItem.name)} ${dim(`(${deckItem.type})`)}${versionTag}`)
70
+ console.log(
71
+ ` Found ${bold(deckItem.title || deckItem.name)} ${dim(`(${deckItem.type})`)}${versionTag}`
72
+ )
73
73
 
74
74
  // Resolve all dependencies (slides, layouts, themes, assets)
75
75
  let resolved
@@ -138,7 +138,10 @@ export async function pull(args) {
138
138
  }
139
139
  } else if (!storedHash) {
140
140
  // File exists but not in lockfile (first pull or untracked)
141
- const overwrite = await confirm(` Overwrite ${relativePath}? ${dim("(local changes will be lost)")}`, false)
141
+ const overwrite = await confirm(
142
+ ` Overwrite ${relativePath}? ${dim("(local changes will be lost)")}`,
143
+ false
144
+ )
142
145
  if (!overwrite) {
143
146
  console.log(` ${dim("Skipped")} ${relativePath}`)
144
147
  continue
@@ -151,7 +154,10 @@ export async function pull(args) {
151
154
  mkdirSync(targetDir, { recursive: true })
152
155
  const dataUriPrefix = file.content.match(/^data:[^;]+;base64,/)
153
156
  if (dataUriPrefix) {
154
- writeFileSync(targetPath, Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64"))
157
+ writeFileSync(
158
+ targetPath,
159
+ Buffer.from(file.content.slice(dataUriPrefix[0].length), "base64")
160
+ )
155
161
  } else {
156
162
  writeFileSync(targetPath, file.content, "utf-8")
157
163
  }
@@ -186,6 +192,7 @@ export async function pull(args) {
186
192
  const slides = available.map(s => ({
187
193
  componentName: s.componentName || toPascalCase(s.slug),
188
194
  importPath: `@/slides/${s.slug}`,
195
+ id: s.id,
189
196
  steps: s.steps,
190
197
  section: s.section
191
198
  }))
@@ -193,13 +200,21 @@ export async function pull(args) {
193
200
  transition: deckItem.meta.transition,
194
201
  directionalTransition: deckItem.meta.directionalTransition
195
202
  })
196
- console.log(` ${green("+")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`)
203
+ console.log(
204
+ ` ${green("+")} Replaced ${cyan("deck-config.ts")} ${dim(`(${slides.length} slides)`)}`
205
+ )
197
206
  } else {
198
207
  // Append individual slides
199
208
  for (const s of available) {
200
209
  const componentName = s.componentName || toPascalCase(s.slug)
201
210
  const importPath = `@/slides/${s.slug}`
202
- const updated = addSlideToDeckConfig(cwd, { componentName, importPath, steps: s.steps, section: s.section })
211
+ const updated = addSlideToDeckConfig(cwd, {
212
+ componentName,
213
+ importPath,
214
+ steps: s.steps,
215
+ id: s.id,
216
+ section: s.section
217
+ })
203
218
  if (updated) {
204
219
  console.log(` ${green("+")} Added ${componentName} to ${cyan("deck-config.ts")}`)
205
220
  }
@@ -227,24 +242,37 @@ export async function pull(args) {
227
242
  if (deckItem.id) {
228
243
  try {
229
244
  const annotationsRes = await fetch(`${auth.registry}/api/items/${deckItem.id}/annotations`, {
230
- headers: { Authorization: `Bearer ${auth.token}`, ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {}) }
245
+ headers: {
246
+ Authorization: `Bearer ${auth.token}`,
247
+ ...(auth.organizationId ? { "X-Organization-Id": auth.organizationId } : {})
248
+ }
231
249
  })
232
250
  if (annotationsRes.ok) {
233
251
  const data = await annotationsRes.json()
234
252
  const annotations = data.annotations ?? []
235
253
  if (annotations.length > 0) {
236
- const annotationsFile = { version: 1, annotations: annotations.map(a => ({
237
- id: a.id,
238
- slideIndex: a.slideIndex,
239
- slideTitle: a.slideTitle,
240
- target: a.target,
241
- body: a.body,
242
- createdAt: a.createdAt,
243
- status: a.status,
244
- ...(a.resolution ? { resolution: a.resolution } : {})
245
- })) }
246
- writeFileSync(join(cwd, "annotations.json"), JSON.stringify(annotationsFile, null, 2) + "\n", "utf-8")
247
- console.log(` ${green("+")} ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : 's'})`)}`)
254
+ const annotationsFile = {
255
+ version: 1,
256
+ annotations: annotations.map(a => ({
257
+ id: a.id,
258
+ slideIndex: a.slideIndex,
259
+ ...(a.slideId ? { slideId: a.slideId } : {}),
260
+ slideTitle: a.slideTitle,
261
+ target: a.target,
262
+ body: a.body,
263
+ createdAt: a.createdAt,
264
+ status: a.status,
265
+ ...(a.resolution ? { resolution: a.resolution } : {})
266
+ }))
267
+ }
268
+ writeFileSync(
269
+ join(cwd, "annotations.json"),
270
+ JSON.stringify(annotationsFile, null, 2) + "\n",
271
+ "utf-8"
272
+ )
273
+ console.log(
274
+ ` ${green("+")} ${cyan("annotations.json")} ${dim(`(${annotations.length} annotation${annotations.length === 1 ? "" : "s"})`)}`
275
+ )
248
276
  }
249
277
  }
250
278
  } catch {
@@ -257,7 +285,9 @@ export async function pull(args) {
257
285
  if (written.length === 0) {
258
286
  console.log(` ${green("+")} Everything is up to date.`)
259
287
  } else {
260
- console.log(` ${green("+")} Pulled ${bold(String(written.length))} file(s) from ${bold(deckSlug)}`)
288
+ console.log(
289
+ ` ${green("+")} Pulled ${bold(String(written.length))} file(s) from ${bold(deckSlug)}`
290
+ )
261
291
  }
262
292
  console.log()
263
293
  closePrompts()
@@ -1,6 +1,7 @@
1
1
  import { createServer } from "vite"
2
2
 
3
3
  import { bold, dim } from "../utils/ansi.mjs"
4
+ import { registerStudioServer } from "../utils/studio-discovery.mjs"
4
5
  import { ensureTsConfig } from "../utils/tsconfig.mjs"
5
6
  import { createViteConfig } from "../vite/config.mjs"
6
7
 
@@ -29,6 +30,8 @@ export async function studio(args) {
29
30
  })
30
31
 
31
32
  await server.listen()
33
+ const baseUrl = server.resolvedUrls?.local[0]
34
+ if (baseUrl) registerStudioServer(cwd, baseUrl)
32
35
  server.printUrls()
33
36
  server.bindCLIShortcuts({ print: true })
34
37
  }
@@ -8,6 +8,8 @@ export async function toImage(args) {
8
8
  const slidePath = args[0]
9
9
  const outputIndex = args.indexOf("-o")
10
10
  const output = outputIndex !== -1 ? args[outputIndex + 1] : null
11
+ const portArg = args.find(arg => arg.startsWith("--port="))
12
+ const studioPort = portArg ? Number(portArg.slice(7)) : undefined
11
13
 
12
14
  if (!slidePath || slidePath === "--help" || slidePath === "-h") {
13
15
  console.log()
@@ -15,6 +17,7 @@ export async function toImage(args) {
15
17
  console.log()
16
18
  console.log(` ${bold("Options:")}`)
17
19
  console.log(` -o <file> Output file path (default: <slide-name>.png)`)
20
+ console.log(` --port=N Studio port to reuse (default: auto-discover)`)
18
21
  console.log()
19
22
  console.log(` ${bold("Examples:")}`)
20
23
  console.log(` ${dim("promptslide to-image src/slides/slide-title.tsx")}`)
@@ -26,7 +29,9 @@ export async function toImage(args) {
26
29
  if (!(await isPlaywrightAvailable())) {
27
30
  console.error()
28
31
  console.error(` ${red("Error:")} Playwright is required for image export.`)
29
- console.error(` Install it with: ${bold("npm install -D playwright && npx playwright install chromium")}`)
32
+ console.error(
33
+ ` Install it with: ${bold("npm install -D playwright && npx playwright install chromium")}`
34
+ )
30
35
  console.error()
31
36
  process.exit(1)
32
37
  }
@@ -36,10 +41,18 @@ export async function toImage(args) {
36
41
  console.log()
37
42
  console.log(` ${dim("Capturing screenshot...")}`)
38
43
 
39
- const buffer = await captureSlideScreenshot({
40
- cwd: process.cwd(),
41
- slidePath
42
- })
44
+ let buffer
45
+ try {
46
+ buffer = await captureSlideScreenshot({
47
+ cwd: process.cwd(),
48
+ slidePath,
49
+ studioPort
50
+ })
51
+ } catch (error) {
52
+ console.error(` ${red("Error:")} ${error.message}`)
53
+ process.exitCode = 1
54
+ return
55
+ }
43
56
 
44
57
  if (!buffer) {
45
58
  console.error(` ${red("Error:")} Failed to capture screenshot.`)
@@ -0,0 +1,27 @@
1
+ import { writeFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+
4
+ import { bold, green, dim, red } from "../utils/ansi.mjs"
5
+ import { captureDeckPdf } from "../utils/export.mjs"
6
+
7
+ export async function toPdf(args) {
8
+ if (args.includes("--help") || args.includes("-h")) {
9
+ console.log(` ${bold("Usage:")} promptslide to-pdf [-o slides.pdf] [--port=N]`)
10
+ return
11
+ }
12
+ const outputIndex = args.indexOf("-o")
13
+ const output = resolve(outputIndex >= 0 ? args[outputIndex + 1] : "slides.pdf")
14
+ const portArg = args.find(arg => arg.startsWith("--port="))
15
+ const studioPort = portArg ? Number(portArg.slice(7)) : undefined
16
+ console.log(` ${dim("Rendering deck PDF...")}`)
17
+ let pdf
18
+ try {
19
+ pdf = await captureDeckPdf({ cwd: process.cwd(), studioPort })
20
+ } catch (error) {
21
+ console.error(` ${red("Error:")} ${error.message}`)
22
+ process.exitCode = 1
23
+ return
24
+ }
25
+ writeFileSync(output, pdf)
26
+ console.log(` ${green("✓")} Saved to ${bold(output)}`)
27
+ }
@@ -48,7 +48,7 @@ const animationVariants: Record<AnimationType, Variants> = {
48
48
  }
49
49
 
50
50
  interface AnimatedProps {
51
- /** Which step reveals this content (1-indexed) */
51
+ /** Which step reveals this content (0 = visible from slide entry) */
52
52
  step: number
53
53
  /** Animation type */
54
54
  animation?: AnimationType
@@ -83,7 +83,7 @@ export function Animated({
83
83
 
84
84
  return (
85
85
  <motion.div
86
- initial="hidden"
86
+ initial={isVisible ? false : "hidden"}
87
87
  animate={isVisible ? "visible" : "hidden"}
88
88
  variants={animationVariants[animation]}
89
89
  transition={{
@@ -138,7 +138,7 @@ export function AnimatedGroup({
138
138
 
139
139
  return (
140
140
  <motion.div
141
- initial="hidden"
141
+ initial={isVisible ? false : "hidden"}
142
142
  animate={isVisible ? "visible" : "hidden"}
143
143
  variants={containerVariants}
144
144
  className={className}
@@ -38,6 +38,14 @@ export function createHttpAdapter(): AnnotationStorageAdapter {
38
38
  async remove(id) {
39
39
  cache = cache.filter(a => a.id !== id)
40
40
  await saveAll(cache)
41
+ },
42
+ async update(id, patch) {
43
+ cache = cache.map(a => (a.id === id ? { ...a, ...patch } : a))
44
+ await saveAll(cache)
45
+ },
46
+ async replaceAll(annotations) {
47
+ cache = annotations
48
+ await saveAll(cache)
41
49
  }
42
50
  }
43
51
  }
@@ -1,9 +1,11 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react"
2
+
2
3
  import type { SlideConfig } from "../types"
4
+ import type { Annotation, AnnotationTarget } from "./types"
5
+
3
6
  import { AnnotationForm } from "./annotation-form"
4
7
  import { AnnotationPin } from "./annotation-pin"
5
8
  import { buildElementTarget, resolveTarget } from "./selectors"
6
- import type { Annotation, AnnotationTarget } from "./types"
7
9
 
8
10
  interface AnnotationOverlayProps {
9
11
  slides: SlideConfig[]
@@ -11,10 +13,17 @@ interface AnnotationOverlayProps {
11
13
  /** Ref to the slide container element (the div wrapping SlideRenderer) */
12
14
  slideContainerRef: React.RefObject<HTMLDivElement | null>
13
15
  selectedId: string | null
16
+ hoveredId: string | null
14
17
  onSelectId: (id: string | null) => void
15
18
  onShowPanel: () => void
16
19
  slideAnnotations: Annotation[]
17
- addAnnotation: (slideIndex: number, slideTitle: string, target: AnnotationTarget, body: string) => void
20
+ addAnnotation: (
21
+ slideIndex: number,
22
+ slideTitle: string,
23
+ target: AnnotationTarget,
24
+ body: string,
25
+ slideId?: string
26
+ ) => void
18
27
  }
19
28
 
20
29
  interface PendingAnnotation {
@@ -23,7 +32,17 @@ interface PendingAnnotation {
23
32
  yPercent: number
24
33
  }
25
34
 
26
- export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, selectedId, onSelectId, onShowPanel, slideAnnotations, addAnnotation }: AnnotationOverlayProps) {
35
+ export function AnnotationOverlay({
36
+ slides,
37
+ currentSlide,
38
+ slideContainerRef,
39
+ selectedId,
40
+ hoveredId,
41
+ onSelectId,
42
+ onShowPanel,
43
+ slideAnnotations,
44
+ addAnnotation
45
+ }: AnnotationOverlayProps) {
27
46
  const [pending, setPending] = useState<PendingAnnotation | null>(null)
28
47
  const [hoveredElement, setHoveredElement] = useState<DOMRect | null>(null)
29
48
  const overlayRef = useRef<HTMLDivElement>(null)
@@ -34,19 +53,31 @@ export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, sel
34
53
  const resolvedAnnotations = slideAnnotations.map((a, i) => {
35
54
  const container = slideContainerRef.current
36
55
  if (!container) {
37
- return { annotation: a, xPercent: a.target.position.xPercent, yPercent: a.target.position.yPercent, number: i + 1 }
56
+ return {
57
+ annotation: a,
58
+ xPercent: a.target.position.xPercent,
59
+ yPercent: a.target.position.yPercent,
60
+ number: i + 1
61
+ }
38
62
  }
39
63
 
40
64
  const { element } = resolveTarget(a.target, container)
41
65
  if (element) {
42
66
  const rect = element.getBoundingClientRect()
43
67
  const containerRect = container.getBoundingClientRect()
44
- const xPercent = ((rect.left + rect.width / 2 - containerRect.left) / containerRect.width) * 100
45
- const yPercent = ((rect.top + rect.height / 2 - containerRect.top) / containerRect.height) * 100
68
+ const xPercent =
69
+ ((rect.left + rect.width / 2 - containerRect.left) / containerRect.width) * 100
70
+ const yPercent =
71
+ ((rect.top + rect.height / 2 - containerRect.top) / containerRect.height) * 100
46
72
  return { annotation: a, xPercent, yPercent, number: i + 1 }
47
73
  }
48
74
 
49
- return { annotation: a, xPercent: a.target.position.xPercent, yPercent: a.target.position.yPercent, number: i + 1 }
75
+ return {
76
+ annotation: a,
77
+ xPercent: a.target.position.xPercent,
78
+ yPercent: a.target.position.yPercent,
79
+ number: i + 1
80
+ }
50
81
  })
51
82
 
52
83
  const handleOverlayClick = useCallback(
@@ -68,7 +99,11 @@ export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, sel
68
99
 
69
100
  // Pick the most specific meaningful element (skip tiny text nodes, pick their parent)
70
101
  let target = elementUnder
71
- if (target.tagName === "SPAN" && target.parentElement && container.contains(target.parentElement)) {
102
+ if (
103
+ target.tagName === "SPAN" &&
104
+ target.parentElement &&
105
+ container.contains(target.parentElement)
106
+ ) {
72
107
  // For inline spans, prefer their parent for a more meaningful target
73
108
  const parent = target.parentElement
74
109
  if (parent.tagName !== "DIV" || parent.children.length <= 3) {
@@ -90,11 +125,11 @@ export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, sel
90
125
  const handleSubmit = useCallback(
91
126
  (text: string) => {
92
127
  if (!pending) return
93
- addAnnotation(currentSlide, slideTitle, pending.target, text)
128
+ addAnnotation(currentSlide, slideTitle, pending.target, text, slides[currentSlide]?.id)
94
129
  setPending(null)
95
130
  onShowPanel()
96
131
  },
97
- [pending, currentSlide, slideTitle, addAnnotation, onShowPanel]
132
+ [pending, currentSlide, slideTitle, slides, addAnnotation, onShowPanel]
98
133
  )
99
134
 
100
135
  // Track hover for element highlighting
@@ -116,12 +151,14 @@ export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, sel
116
151
  if (elementUnder && container.contains(elementUnder) && elementUnder !== container) {
117
152
  const containerRect = container.getBoundingClientRect()
118
153
  const elRect = elementUnder.getBoundingClientRect()
154
+ const scaleX = containerRect.width / container.offsetWidth
155
+ const scaleY = containerRect.height / container.offsetHeight
119
156
  setHoveredElement(
120
157
  new DOMRect(
121
- elRect.left - containerRect.left,
122
- elRect.top - containerRect.top,
123
- elRect.width,
124
- elRect.height
158
+ (elRect.left - containerRect.left) / scaleX,
159
+ (elRect.top - containerRect.top) / scaleY,
160
+ elRect.width / scaleX,
161
+ elRect.height / scaleY
125
162
  )
126
163
  )
127
164
  } else {
@@ -159,29 +196,38 @@ export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, sel
159
196
  role="button"
160
197
  tabIndex={0}
161
198
  className="absolute inset-0 z-20"
162
- style={{ cursor: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none'%3E%3Ccircle cx='12' cy='12' r='8' stroke='%23FF6B35' stroke-width='2' opacity='0.8'/%3E%3Ccircle cx='12' cy='12' r='2' fill='%23FF6B35'/%3E%3C/svg%3E") 12 12, crosshair` }}
199
+ style={{
200
+ cursor: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none'%3E%3Ccircle cx='12' cy='12' r='8' stroke='%23FF6B35' stroke-width='2' opacity='0.8'/%3E%3Ccircle cx='12' cy='12' r='2' fill='%23FF6B35'/%3E%3C/svg%3E") 12 12, crosshair`
201
+ }}
163
202
  onClick={handleOverlayClick}
164
- onKeyDown={e => { if (e.key === "Escape") { setPending(null); onSelectId(null) } }}
203
+ onKeyDown={e => {
204
+ if (e.key === "Escape") {
205
+ setPending(null)
206
+ onSelectId(null)
207
+ }
208
+ }}
165
209
  onMouseMove={handleMouseMove}
166
210
  onMouseLeave={() => setHoveredElement(null)}
167
211
  />
168
212
 
169
213
  {/* Annotation pins */}
170
- {resolvedAnnotations.map(({ annotation, xPercent, yPercent, number }) => (
171
- <AnnotationPin
172
- key={annotation.id}
173
- number={number}
174
- status={annotation.status}
175
- xPercent={xPercent}
176
- yPercent={yPercent}
177
- isSelected={annotation.id === selectedId}
178
- onClick={() => {
179
- onSelectId(annotation.id === selectedId ? null : annotation.id)
180
- onShowPanel()
181
- setPending(null)
182
- }}
183
- />
184
- ))}
214
+ {resolvedAnnotations
215
+ .filter(({ annotation }) => annotation.status === "open" || annotation.id === hoveredId)
216
+ .map(({ annotation, xPercent, yPercent, number }) => (
217
+ <AnnotationPin
218
+ key={annotation.id}
219
+ number={number}
220
+ status={annotation.status}
221
+ xPercent={xPercent}
222
+ yPercent={yPercent}
223
+ isSelected={annotation.id === selectedId}
224
+ onClick={() => {
225
+ onSelectId(annotation.id === selectedId ? null : annotation.id)
226
+ onShowPanel()
227
+ setPending(null)
228
+ }}
229
+ />
230
+ ))}
185
231
 
186
232
  {/* New annotation form */}
187
233
  {pending && (
@@ -192,7 +238,6 @@ export function AnnotationOverlay({ slides, currentSlide, slideContainerRef, sel
192
238
  onCancel={() => setPending(null)}
193
239
  />
194
240
  )}
195
-
196
241
  </>
197
242
  )
198
243
  }