promptslide 0.3.17 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "promptslide",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "CLI and slide engine for PromptSlide presentations",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- },
13
- "./src/commands/*": "./src/commands/*"
5
+ "keywords": [
6
+ "cli",
7
+ "presentations",
8
+ "promptslide",
9
+ "react",
10
+ "slides",
11
+ "vite"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "prompticeu",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/prompticeu/promptslide.git"
14
18
  },
15
19
  "bin": {
16
20
  "promptslide": "src/index.mjs"
@@ -22,47 +26,43 @@
22
26
  "CHANGELOG.md",
23
27
  "README.md"
24
28
  ],
29
+ "type": "module",
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ },
37
+ "./src/commands/*": "./src/commands/*"
38
+ },
25
39
  "scripts": {
26
40
  "build": "tsup",
27
41
  "test": "node --test test/*.test.mjs",
28
42
  "prepublishOnly": "npm run build"
29
43
  },
30
- "license": "MIT",
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/prompticeu/promptslide.git"
34
- },
35
- "author": "prompticeu",
36
- "keywords": [
37
- "slides",
38
- "presentations",
39
- "cli",
40
- "react",
41
- "promptslide",
42
- "vite"
43
- ],
44
44
  "dependencies": {
45
45
  "@tailwindcss/postcss": "^4.1.13",
46
46
  "@vitejs/plugin-react": "^4.5.2",
47
47
  "clsx": "^2.1.1",
48
48
  "lucide-react": "^0.552.0",
49
+ "playwright": "^1.50.0",
49
50
  "postcss": "^8.5.6",
50
51
  "tailwind-merge": "^3.3.1",
51
52
  "tailwindcss": "^4.1.13",
52
- "playwright": "^1.50.0",
53
+ "typescript": "^5.9.3",
53
54
  "vite": "^6.3.5"
54
55
  },
56
+ "devDependencies": {
57
+ "@types/react": "^19.2.14",
58
+ "@types/react-dom": "^19.2.3",
59
+ "tsup": "^8.5.1"
60
+ },
55
61
  "peerDependencies": {
56
62
  "framer-motion": "^12.0.0",
57
63
  "react": "^19.0.0",
58
64
  "react-dom": "^19.0.0"
59
65
  },
60
- "devDependencies": {
61
- "@types/react": "^19.2.14",
62
- "@types/react-dom": "^19.2.3",
63
- "tsup": "^8.5.1",
64
- "typescript": "^5.9.3"
65
- },
66
66
  "engines": {
67
67
  "node": ">=18.0.0"
68
68
  }
@@ -11,7 +11,7 @@ export async function studio(args) {
11
11
  const port = portArg ? parseInt(portArg.split("=")[1], 10) : 5173
12
12
  const hasHost = args.includes("--host") || args.some(a => a.startsWith("--host="))
13
13
  const hostArg = args.find(a => a.startsWith("--host="))
14
- const host = hasHost ? (hostArg ? hostArg.split("=")[1] || "0.0.0.0" : "0.0.0.0") : undefined
14
+ const host = hasHost ? (hostArg ? hostArg.split("=")[1] || "0.0.0.0" : "0.0.0.0") : "127.0.0.1"
15
15
 
16
16
  ensureTsConfig(cwd)
17
17
 
@@ -19,13 +19,14 @@ export async function studio(args) {
19
19
  console.log(` ${bold("promptslide")} ${dim("studio")}`)
20
20
  console.log()
21
21
 
22
- const config = createViteConfig({ cwd, mode: "development" })
22
+ const config = createViteConfig({ cwd, mode: "development", editor: !hasHost })
23
23
  const server = await createServer({
24
24
  ...config,
25
25
  server: {
26
26
  port,
27
+ host,
27
28
  strictPort: false,
28
- ...(host && { host, allowedHosts: true })
29
+ ...(hasHost && { allowedHosts: true })
29
30
  }
30
31
  })
31
32
 
@@ -238,6 +238,26 @@ export function SlideDeck({
238
238
  )
239
239
 
240
240
  const [viewMode, setViewMode] = useState<ViewMode>("slide")
241
+ const [isEditorOpen, setIsEditorOpen] = useState(false)
242
+ const [editorRevision, setEditorRevision] = useState(0)
243
+ useEffect(() => {
244
+ const onEditorState = (event: Event) => {
245
+ const open = Boolean((event as CustomEvent).detail?.open)
246
+ setIsEditorOpen(open)
247
+ if (open) {
248
+ setViewMode("slide")
249
+ setIsAnnotationMode(false)
250
+ setShowAnnotationPanel(false)
251
+ }
252
+ }
253
+ const refreshEditor = () => setEditorRevision(value => value + 1)
254
+ window.addEventListener("promptslide:editor-state", onEditorState)
255
+ window.addEventListener("promptslide:editor-refresh", refreshEditor)
256
+ return () => {
257
+ window.removeEventListener("promptslide:editor-state", onEditorState)
258
+ window.removeEventListener("promptslide:editor-refresh", refreshEditor)
259
+ }
260
+ }, [])
241
261
  const [isPresentationMode, setIsPresentationMode] = useState(false)
242
262
  const [viewportWidth, setViewportWidth] = useState(() =>
243
263
  typeof window === "undefined" ? 1280 : window.innerWidth
@@ -512,6 +532,7 @@ export function SlideDeck({
512
532
  if (e.altKey || e.ctrlKey || e.metaKey) return
513
533
  const key = e.key.toLowerCase()
514
534
  if (e.repeat && "fvglcdt".includes(key)) return
535
+ if (isEditorOpen && ["f", "v", "g", "l", "c", "d"].includes(key)) return
515
536
 
516
537
  if (key === "f") {
517
538
  e.preventDefault()
@@ -574,6 +595,7 @@ export function SlideDeck({
574
595
  viewMode,
575
596
  togglePresentationMode,
576
597
  isAnnotationMode,
598
+ isEditorOpen,
577
599
  isPresentationMode,
578
600
  setThumbnailRailOpen,
579
601
  showThumbnailRail,
@@ -581,9 +603,33 @@ export function SlideDeck({
581
603
  handleExportPdf
582
604
  ])
583
605
 
606
+ const editorPanelWidth = viewportWidth <= 850 ? 280 : 320
607
+
584
608
  return (
585
609
  <div className="min-h-screen w-full bg-neutral-950 text-foreground">
586
610
  <style>{`
611
+ .slide-thumbnail-scroll-area {
612
+ scrollbar-width: thin;
613
+ scrollbar-color: #404040 transparent;
614
+ }
615
+ .slide-thumbnail-scroll-area::-webkit-scrollbar {
616
+ width: 10px;
617
+ }
618
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-track {
619
+ background: transparent;
620
+ }
621
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-thumb {
622
+ background-color: #404040;
623
+ border: 3px solid transparent;
624
+ border-radius: 9999px;
625
+ background-clip: padding-box;
626
+ }
627
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-thumb:hover {
628
+ background-color: #525252;
629
+ }
630
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-corner {
631
+ background: transparent;
632
+ }
587
633
  @media print {
588
634
  .list-slide-canvas {
589
635
  transform: scale(1.5) !important;
@@ -610,11 +656,19 @@ export function SlideDeck({
610
656
 
611
657
  {/* Toolbar */}
612
658
  <div
659
+ data-ps-viewer-toolbar
660
+ style={
661
+ isEditorOpen
662
+ ? { right: editorPanelWidth + 16, maxWidth: `calc(100vw - ${editorPanelWidth + 32}px)` }
663
+ : undefined
664
+ }
613
665
  className={cn(
614
666
  "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] duration-200 ease-out print:hidden",
615
667
  (isPresentationMode || (isNarrowViewport && isAnnotationMode && showAnnotationPanel)) &&
616
668
  "hidden",
617
- isAnnotationMode && showAnnotationPanel ? "right-[21rem]" : "right-4"
669
+ isEditorOpen && "flex-wrap",
670
+ isEditorOpen && viewportWidth <= 850 && "[&_button:disabled]:hidden [&>div]:hidden",
671
+ !isEditorOpen && (isAnnotationMode && showAnnotationPanel ? "right-[21rem]" : "right-4")
618
672
  )}
619
673
  >
620
674
  <button
@@ -623,6 +677,7 @@ export function SlideDeck({
623
677
  "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
624
678
  viewMode === "slide" && "bg-neutral-800 text-white"
625
679
  )}
680
+ disabled={isEditorOpen}
626
681
  title="Presentation View (V)"
627
682
  >
628
683
  <Monitor className="h-4 w-4" />
@@ -633,6 +688,7 @@ export function SlideDeck({
633
688
  "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
634
689
  viewMode === "list" && "bg-neutral-800 text-white"
635
690
  )}
691
+ disabled={isEditorOpen}
636
692
  title="List View (L)"
637
693
  >
638
694
  <List className="h-4 w-4" />
@@ -643,6 +699,7 @@ export function SlideDeck({
643
699
  "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
644
700
  viewMode === "grid" && "bg-neutral-800 text-white"
645
701
  )}
702
+ disabled={isEditorOpen}
646
703
  title="Grid View (G)"
647
704
  >
648
705
  <Grid3X3 className="h-4 w-4" />
@@ -653,14 +710,18 @@ export function SlideDeck({
653
710
  <button
654
711
  onClick={toggleCommentMode}
655
712
  className={cn(
656
- "relative inline-flex h-8 items-center justify-center rounded-md transition-colors",
713
+ "relative inline-flex h-8 items-center justify-center gap-1.5 rounded-md font-[family-name:system-ui] text-[13px] leading-none font-medium transition-colors",
657
714
  isAnnotationMode
658
715
  ? "border border-[#FF6B35]/50 bg-[#FF6B35]/15 p-[7px] text-[#FF6B35] hover:border-[#FF6B35] hover:bg-[#FF6B35]/25"
659
- : "p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white"
716
+ : "px-2.5 text-neutral-400 hover:bg-neutral-800 hover:text-white"
660
717
  )}
718
+ disabled={isEditorOpen}
719
+ aria-label="Comment"
720
+ aria-pressed={isAnnotationMode}
661
721
  title="Comment (C)"
662
722
  >
663
- <MessageCircle className="h-4 w-4" />
723
+ <MessageCircle className="h-[15px] w-[15px] shrink-0" />
724
+ {!isAnnotationMode && <span>Comment</span>}
664
725
  {openCount > 0 && (
665
726
  <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">
666
727
  {openCount}
@@ -668,11 +729,14 @@ export function SlideDeck({
668
729
  )}
669
730
  </button>
670
731
 
732
+ <span data-ps-editor-toolbar className="contents" />
733
+
671
734
  <div className="mx-1 w-px bg-neutral-800" />
672
735
 
673
736
  <button
674
737
  onClick={handleExportPdf}
675
738
  className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
739
+ disabled={isEditorOpen}
676
740
  title="Download PDF (D)"
677
741
  >
678
742
  <Download className="h-4 w-4" />
@@ -680,6 +744,7 @@ export function SlideDeck({
680
744
  <button
681
745
  onClick={togglePresentationMode}
682
746
  className="inline-flex h-8 items-center justify-center gap-1.5 rounded-md border border-[#FF6B35]/50 bg-[#FF6B35]/15 px-3 text-sm font-semibold text-[#FF6B35] transition-colors hover:border-[#FF6B35] hover:bg-[#FF6B35]/25"
747
+ disabled={isEditorOpen}
683
748
  title="Present (F)"
684
749
  >
685
750
  <Play className="h-3.5 w-3.5 fill-current" />
@@ -730,7 +795,7 @@ export function SlideDeck({
730
795
  <PanelLeftClose className="h-4 w-4" />
731
796
  </button>
732
797
  </div>
733
- <div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-3">
798
+ <div className="slide-thumbnail-scroll-area min-h-0 flex-1 space-y-3 overflow-y-auto p-3">
734
799
  {slides.map((slideConfig, index) => (
735
800
  <button
736
801
  key={slideConfig.id ?? index}
@@ -883,7 +948,10 @@ export function SlideDeck({
883
948
  >
884
949
  <div
885
950
  ref={slideContainerRef}
886
- className="absolute top-0 left-0 overflow-hidden"
951
+ data-ps-editor-canvas
952
+ tabIndex={-1}
953
+ data-slide-index={currentSlide}
954
+ className="absolute top-0 left-0 overflow-hidden outline-none"
887
955
  style={{
888
956
  width: SLIDE_DIMENSIONS.width,
889
957
  height: SLIDE_DIMENSIONS.height,
@@ -891,6 +959,7 @@ export function SlideDeck({
891
959
  }}
892
960
  >
893
961
  <SlideRenderer
962
+ key={editorRevision}
894
963
  slides={slides}
895
964
  currentSlide={currentSlide}
896
965
  animationStep={animationStep}
@@ -915,6 +984,10 @@ export function SlideDeck({
915
984
  />
916
985
  )}
917
986
  </div>
987
+ <div
988
+ data-ps-editor-overlay
989
+ className="pointer-events-none absolute inset-0 z-20"
990
+ />
918
991
  </div>
919
992
  )}
920
993
  </LayoutGroup>
@@ -945,6 +1018,15 @@ export function SlideDeck({
945
1018
  )}
946
1019
  </div>
947
1020
 
1021
+ <div
1022
+ data-ps-editor-panel
1023
+ data-open={isEditorOpen}
1024
+ aria-hidden={!isEditorOpen}
1025
+ inert={!isEditorOpen}
1026
+ className="h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out motion-reduce:transition-none"
1027
+ style={{ width: isEditorOpen ? editorPanelWidth : 0 }}
1028
+ />
1029
+
948
1030
  {/* Annotation Panel — beside the slide */}
949
1031
  {!isPresentationMode && (
950
1032
  <div
@@ -58,6 +58,7 @@ export function SlideRenderer({
58
58
  <AnimatePresence initial={false}>
59
59
  <motion.div
60
60
  key={currentSlide}
61
+ data-slide-render-index={currentSlide}
61
62
  variants={slideVariants}
62
63
  initial="enter"
63
64
  animate="center"
@@ -75,14 +76,8 @@ export function SlideRenderer({
75
76
  totalSteps={totalSteps}
76
77
  showAllAnimations={showAllAnimations}
77
78
  >
78
- <SlideErrorBoundary
79
- slideIndex={currentSlide}
80
- slideTitle={slides[currentSlide]?.title}
81
- >
82
- <CurrentSlideComponent
83
- slideNumber={currentSlide + 1}
84
- totalSlides={slides.length}
85
- />
79
+ <SlideErrorBoundary slideIndex={currentSlide} slideTitle={slides[currentSlide]?.title}>
80
+ <CurrentSlideComponent slideNumber={currentSlide + 1} totalSlides={slides.length} />
86
81
  </SlideErrorBoundary>
87
82
  </AnimationProvider>
88
83
  </motion.div>
@@ -0,0 +1,35 @@
1
+ # Development source editor
2
+
3
+ Run `promptslide studio` and choose **Edit** in the existing top-right toolbar. The editor is injected by the development Vite plugin. Production builds, registry previews, embeds, and export pages do not load it. No editable-component migration is needed.
4
+
5
+ ## Workflow
6
+
7
+ - Double-click literal text to edit it in place. Select an existing element, or use Slide elements. Shift-click selects several elements. Double-click a selected group to reach the element under the pointer; click outside the slide to deselect.
8
+ - Edit literal text and title/subtitle/eyebrow properties, appearance, image URLs, or public asset paths in Design. Arrange contains positioning and alignment controls.
9
+ - Drag selection outlines to move. The lower handle resizes; the upper handle rotates. Arrow keys nudge by one design pixel, Shift by ten. Alt disables snapping. All geometry is measured in the 1280 × 720 design canvas.
10
+ - Align selected elements along either axis or distribute three or more with equal gaps. Bring forward / Send back set stacking order.
11
+ - For images, adjust the frame size, objectFit, objectPosition, and inset crop.
12
+ - Select a range in Text and apply bold, italic, or underline. Save existing text changes first. Range formatting creates ordinary styled JSX spans, which can subsequently be selected individually.
13
+ - Structure supports inserting text, shapes, and images into JSX containers and duplicating/deleting direct JSX children. Root components and expressions cannot be deleted. Duplicates preview immediately and support Undo and Discard; save to edit copies independently. Insertions render after Save; range formatting previews immediately.
14
+ - Undo/redo affects the buffered draft. Save batches edits to source; Discard restores the source rendering. Close requires resolving unsaved changes. The browser tab retains its draft across hot reloads. Export draft downloads the pending operations for recovery.
15
+ - Editing uses the original viewer canvas, styling, and animation wrappers. Leave Edit to reveal steps normally; opening Edit preserves the currently visible content.
16
+
17
+ ## Source semantics
18
+
19
+ Instrumentation adds source coordinates and a SHA-256 file revision only to the transformed development module. The original TSX is unchanged until Save. Intrinsic DOM elements and `motion.*` elements are supported. Source files must resolve inside the project's `src/` directory; symlinks outside it are rejected.
20
+
21
+ A style change belongs to a JSX definition, so it affects all instances of that definition, including shared layouts. Movement uses the individual CSS `translate` property, preserving normal flex/grid flow and existing Motion transforms; rotation uses `rotate`. Resizing sets explicit frame dimensions. It does not convert the deck into an absolute-positioned document.
22
+
23
+ For direct local `array.map(item => <p>{item.title}</p>)` expressions over literal object arrays, text edits address the selected array item. Imported arrays, spreads, ambiguous bindings, computed expressions, and mixed children are intentionally read-only. Edit those expressions in code. Simple local component text props are resolved automatically when their rendered text is selected, without requiring the component to forward DOM metadata. Ambiguous or imported prop relationships remain unsupported. Shared appearance controls require opting in when multiple matching instances are present on the current slide.
24
+
25
+ Every batch checks all file revisions and validates all resulting TSX syntax before writing. Stale edits receive HTTP 409. Export or discard the draft and reselect from the current source; there is no force-overwrite button. Deleting an element discards pending edits on that element and its descendants. Other parent/child structural edits that overlap must be saved separately. This is optimistic concurrency protection, not a filesystem transaction across external processes. A save failure rolls back files still matching this operation's output.
26
+
27
+ The endpoint uses a per-server random token, checks cross-site fetches, limits payload size and operation counts, and only runs in Vite serve mode. Studio enables editing on its default loopback interface. Passing `--host` (including `--host=...`) disables the editor, source instrumentation, and editor endpoint so shared previews cannot edit source files. The client token is a request guard, not authentication for network clients.
28
+
29
+ ## Verification
30
+
31
+ - `node --test packages/cli/test/editor.test.mjs`: source edits, escaping, conflicts, batch validation, path restrictions, and array instances.
32
+ - `node packages/cli/test/editor.browser.mjs`: isolated temporary deck; text/history, save/reload, concurrent edits, scaled dragging, and production bundle exclusion. Requires the package's Playwright Chromium installation.
33
+ - `tsc -p packages/cli/tsconfig.json --noEmit`: editor and engine types.
34
+
35
+ Implementation uses TypeScript's JSX parser and original-range patches. No OpenSlide source code is vendored.
@@ -0,0 +1,76 @@
1
+ export interface Rect {
2
+ x: number
3
+ y: number
4
+ width: number
5
+ height: number
6
+ }
7
+ export function translated(value: string, dx: number, dy: number) {
8
+ // Computed translate may contain percentages or calc() with spaces. Keep
9
+ // those units so movement does not remove centering or break after resizing.
10
+ const axes: string[] = []
11
+ let depth = 0,
12
+ start = 0
13
+ const input = value.trim()
14
+ for (let i = 0; i <= input.length; i++) {
15
+ const char = input[i]
16
+ if (char === "(") depth++
17
+ if (char === ")") depth--
18
+ if (i === input.length || (depth === 0 && /\s/.test(char!))) {
19
+ if (i > start) axes.push(input.slice(start, i))
20
+ start = i + 1
21
+ }
22
+ }
23
+ if (axes[0] === "none") axes.length = 0
24
+ const offset = (axis: string, delta: number) => {
25
+ if (!delta) return axis
26
+ if (/^-?(?:\d*\.)?\d+px$/.test(axis)) return `${parseFloat(axis) + delta}px`
27
+ return `calc(${axis} ${delta < 0 ? "-" : "+"} ${Math.abs(delta)}px)`
28
+ }
29
+ return [offset(axes[0] || "0px", dx), offset(axes[1] || "0px", dy), ...axes.slice(2)].join(" ")
30
+ }
31
+ export function snap(rect: Rect, others: Rect[], tolerance = 6) {
32
+ const xs = [0, 640, 1280, ...others.flatMap(r => [r.x, r.x + r.width / 2, r.x + r.width])]
33
+ const ys = [0, 360, 720, ...others.flatMap(r => [r.y, r.y + r.height / 2, r.y + r.height])]
34
+ const closest = (values: number[], targets: number[]) => {
35
+ let delta = tolerance + 1
36
+ let guide: number | undefined
37
+ for (const v of values)
38
+ for (const t of targets)
39
+ if (Math.abs(t - v) < Math.abs(delta)) {
40
+ delta = t - v
41
+ guide = t
42
+ }
43
+ return Math.abs(delta) <= tolerance ? { delta, guide } : { delta: 0, guide: undefined }
44
+ }
45
+ const x = closest([rect.x, rect.x + rect.width / 2, rect.x + rect.width], xs)
46
+ const y = closest([rect.y, rect.y + rect.height / 2, rect.y + rect.height], ys)
47
+ return { x: rect.x + x.delta, y: rect.y + y.delta, guideX: x.guide, guideY: y.guide }
48
+ }
49
+ export function aligned(
50
+ rects: Rect[],
51
+ axis: "x" | "y",
52
+ mode: "start" | "center" | "end" | "distribute"
53
+ ) {
54
+ const size = axis === "x" ? "width" : "height"
55
+ if (mode === "distribute") {
56
+ if (rects.length < 3) return rects.map(r => r[axis])
57
+ const sorted = rects.map((r, i) => ({ ...r, i })).sort((a, b) => a[axis] - b[axis])
58
+ const first = sorted[0]!,
59
+ last = sorted[sorted.length - 1]!
60
+ const gap =
61
+ (last[axis] + last[size] - first[axis] - sorted.reduce((s, r) => s + r[size], 0)) /
62
+ (rects.length - 1)
63
+ const result: number[] = []
64
+ let pos = first[axis]
65
+ for (const r of sorted) {
66
+ result[r.i] = pos
67
+ pos += r[size] + gap
68
+ }
69
+ return result
70
+ }
71
+ const min = Math.min(...rects.map(r => r[axis])),
72
+ max = Math.max(...rects.map(r => r[axis] + r[size]))
73
+ return rects.map(r =>
74
+ mode === "start" ? min : mode === "end" ? max - r[size] : (min + max - r[size]) / 2
75
+ )
76
+ }
@@ -0,0 +1,24 @@
1
+ import { createElement } from "react"
2
+ import { createRoot } from "react-dom/client"
3
+
4
+ import { EditorLauncher } from "./studio"
5
+
6
+ // Keep mounting outside the component module so React Fast Refresh can retain
7
+ // the open editor, its draft, and its history when the sidebar code changes.
8
+ export function mountEditor(
9
+ _slides: unknown,
10
+ _theme: unknown,
11
+ token: string,
12
+ initiallyOpen = false
13
+ ) {
14
+ if (document.getElementById("promptslide-editor-root")) return
15
+ const host = document.createElement("div")
16
+ host.id = "promptslide-editor-root"
17
+ document.body.appendChild(host)
18
+ const root = createRoot(host)
19
+ root.render(createElement(EditorLauncher, { token, initiallyOpen }))
20
+ return () => {
21
+ root.unmount()
22
+ host.remove()
23
+ }
24
+ }