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/CHANGELOG.md +7 -0
- package/dist/index.js +134 -63
- package/dist/index.js.map +1 -1
- package/package.json +31 -31
- package/src/commands/studio.mjs +4 -3
- package/src/core/slide-deck.tsx +88 -6
- package/src/core/slide-renderer.tsx +3 -8
- package/src/editor/README.md +35 -0
- package/src/editor/geometry.ts +76 -0
- package/src/editor/mount.ts +24 -0
- package/src/editor/plugin.mjs +189 -0
- package/src/editor/source.mjs +363 -0
- package/src/editor/studio.css +1004 -0
- package/src/editor/studio.tsx +1948 -0
- package/src/vite/config.mjs +23 -2
- package/src/vite/plugin.mjs +3 -2
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptslide",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "CLI and slide engine for PromptSlide presentations",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
"
|
|
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
|
}
|
package/src/commands/studio.mjs
CHANGED
|
@@ -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") :
|
|
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
|
-
...(
|
|
29
|
+
...(hasHost && { allowedHosts: true })
|
|
29
30
|
}
|
|
30
31
|
})
|
|
31
32
|
|
package/src/core/slide-deck.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
: "
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|