From afa445a2d00eb157d49c5cfbac370fa5758fda79 Mon Sep 17 00:00:00 2001 From: MUTHYALA HARI MOULI <123249040+harimouli@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:29:46 +0530 Subject: [PATCH] feat: improve snapshot editor UX --- .../snapshot/components/SnapshotPreview.tsx | 37 +-- .../snapshot/components/SnapshotStudio.tsx | 221 +++++++++--------- 2 files changed, 137 insertions(+), 121 deletions(-) diff --git a/apps/the_monkeys/src/features/snapshot/components/SnapshotPreview.tsx b/apps/the_monkeys/src/features/snapshot/components/SnapshotPreview.tsx index 1b5c260cc..c2aca9476 100644 --- a/apps/the_monkeys/src/features/snapshot/components/SnapshotPreview.tsx +++ b/apps/the_monkeys/src/features/snapshot/components/SnapshotPreview.tsx @@ -16,14 +16,9 @@ export interface SnapshotPreviewProps { /** Background "stage" colour around the canvas. */ stageBackground?: string; className?: string; + previewHeight?: number; } -/** - * Renders the selected template at its native px size inside a fixed-size - * wrapper, then scales the wrapper with CSS `transform` so the visible preview - * fits the available area without affecting the export pipeline (which still - * reads the native dimensions via a ref). - */ export const SnapshotPreview = forwardRef( function SnapshotPreview( { @@ -34,6 +29,7 @@ export const SnapshotPreview = forwardRef( maxPreviewWidth, stageBackground = 'transparent', className, + previewHeight, }, ref ) { @@ -44,20 +40,33 @@ export const SnapshotPreview = forwardRef( useEffect(() => { if (!stageRef.current) return; + const el = stageRef.current; + const update = () => { - const available = maxPreviewWidth ?? el.clientWidth; - if (!available) return; - const next = Math.min(1, available / template.width); + const availableWidth = maxPreviewWidth ?? el.clientWidth; + + if (!availableWidth) return; + + let next = Math.min(1, availableWidth / template.width); + + if (previewHeight) { + next = Math.min(next, previewHeight / template.height); + } + setScale(next); }; + update(); + const ro = new ResizeObserver(update); ro.observe(el); + return () => ro.disconnect(); - }, [template.width, maxPreviewWidth]); + }, [template.width, maxPreviewWidth, previewHeight]); const Render = template.Render; + const scaledWidth = template.width * scale; const scaledHeight = template.height * scale; @@ -76,15 +85,9 @@ export const SnapshotPreview = forwardRef( overflow: 'hidden', marginLeft: 'auto', marginRight: 'auto', + backgroundColor: stageBackground, }} > - {/* - * The CSS scale lives on this wrapper, NOT on the ref'd - * element below. If we put `transform` on the export root, - * html-to-image would serialize the transform into its - * SVG output and produce a tiny image in the top-left of - * a native-sized canvas. - */}
( 'image' ); - const tweetCanvasSize = TWEET_ASPECT_DIMENSIONS[tweetOptions.aspect]; const tweetId = useMemo(() => parseTweetId(tweetUrl), [tweetUrl]); @@ -139,6 +138,24 @@ export const SnapshotStudio = ({ return () => ro.disconnect(); }, [previewMode, tweetCanvasSize.width]); + const [previewHeight, setPreviewHeight] = useState( + undefined + ); + + useEffect(() => { + const updatePreviewHeight = () => { + setPreviewHeight(Math.max(400, window.innerHeight - 180)); + }; + + updatePreviewHeight(); + + window.addEventListener('resize', updatePreviewHeight); + + return () => { + window.removeEventListener('resize', updatePreviewHeight); + }; + }, []); + const { exportImage, isExporting, error } = useExport(snapshotRef, { width: template.width, height: template.height, @@ -325,116 +342,117 @@ export const SnapshotStudio = ({
-
-
-

- {previewMode === 'x' ? 'X post screenshot' : template.label} -

-

- {previewMode === 'x' - ? tweetId - ? `${tweetCanvasSize.width}×${tweetCanvasSize.height} · Clean screenshot card` - : 'Paste a public X post URL' - : `${template.width}×${template.height} · ${template.aspect}`} -

-
- -
- {previewMode === 'template' ? ( - - ) : ( -
-
+
+
+
+

+ {previewMode === 'x' ? 'X post screenshot' : template.label} +

+ +

+ {previewMode === 'x' + ? tweetId + ? `${tweetCanvasSize.width}×${tweetCanvasSize.height} · Clean screenshot card` + : 'Paste a public X post URL' + : `${template.width}×${template.height} · ${template.aspect}`} +

+
+ +
+ {previewMode === 'template' ? ( + + ) : ( +
- - - {/* Decorative drag handles to replicate layout design */} -
-
+ className='relative' + style={{ + width: tweetCanvasSize.width, + height: tweetCanvasSize.height, + transform: `scale(${xScale})`, + transformOrigin: 'top left', + }} + > + + + {/* Decorative drag handles to replicate layout design */} +
+ +
+ +
+
-
- {/* Stage description footer */} -
-

- But if you like this tool, you can always{' '} - - fund us on github.com.co - -

-

- Issues?{' '} - - Contact us - -

-

- Works instantly on mobile or desktop and every browser -

+ {/* Stage description footer */} +
+

+ But if you like this tool, you can always{' '} + + fund us on github.com.co + +

+ +

+ Issues?{' '} + + Contact us + +

+ +

+ Works instantly on mobile or desktop and every browser +

+
-
- )} + )} +
{activeError ? ( @@ -446,12 +464,7 @@ export const SnapshotStudio = ({ ) : null}
-