Created
December 18, 2025 09:13
-
-
Save svrnm/5983107069437531ddc687d44c33949c to your computer and use it in GitHub Desktop.
A view that can be embeeded into docusaurus to show an exported HTML view of your product instead of a screenshot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useRef } from 'react'; | |
| import { useColorMode } from '@docusaurus/theme-common'; | |
| const DEFAULT_ZOOM = 0.75; // 75% zoom by default | |
| interface SinglePageViewerProps { | |
| src: string; | |
| hint?: string; | |
| defaultZoom?: number; | |
| } | |
| export default function SinglePageViewer({ src, hint, defaultZoom = DEFAULT_ZOOM }: SinglePageViewerProps) { | |
| const { colorMode } = useColorMode(); | |
| const isDarkMode = colorMode === 'dark'; | |
| const [isFullWidth, setIsFullWidth] = useState(false); | |
| const [zoom, setZoom] = useState(defaultZoom); | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| const iframeWrapperRef = useRef<HTMLDivElement>(null); | |
| const toggleFullWidth = () => { | |
| const newFullWidth = !isFullWidth; | |
| setIsFullWidth(newFullWidth); | |
| if (newFullWidth) { | |
| // Reset zoom to 100% when entering full width | |
| setZoom(1.0); | |
| // Use setTimeout to ensure the layout has updated before scrolling | |
| setTimeout(() => { | |
| containerRef.current?.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start', | |
| inline: 'nearest', | |
| }); | |
| // Add a small scroll offset to ensure the frame header is visible | |
| window.scrollBy({ top: -20, behavior: 'smooth' }); | |
| }, 100); | |
| } else { | |
| // Restore default zoom when exiting full width | |
| setZoom(defaultZoom); | |
| } | |
| }; | |
| const openInNewWindow = () => { | |
| window.open(src, '_blank', 'noopener,noreferrer'); | |
| }; | |
| return ( | |
| <> | |
| {isFullWidth && ( | |
| <div | |
| style={{ | |
| position: 'fixed', | |
| top: 0, | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| backgroundColor: 'rgba(0, 0, 0, 0.1)', | |
| zIndex: 99, | |
| pointerEvents: 'none', | |
| }} | |
| /> | |
| )} | |
| <div | |
| ref={containerRef} | |
| style={{ | |
| position: 'relative', | |
| marginBottom: '1rem', | |
| marginTop: isFullWidth ? '1rem' : '0', | |
| width: isFullWidth ? 'calc(100vw - 2rem)' : '100%', | |
| maxWidth: isFullWidth ? 'calc(100vw - 2rem)' : 'none', | |
| marginLeft: isFullWidth ? 'calc(-50vw + 50% + 1rem)' : '0', | |
| marginRight: isFullWidth ? 'calc(-50vw + 50% + 1rem)' : '0', | |
| transition: 'all 0.3s ease', | |
| zIndex: isFullWidth ? 100 : 'auto', | |
| isolation: 'isolate', | |
| boxSizing: 'border-box', | |
| }} | |
| > | |
| <div | |
| style={{ | |
| border: isDarkMode ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)', | |
| borderRadius: '8px', | |
| padding: '1rem', | |
| background: isDarkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(255, 255, 255, 0.7)', | |
| backdropFilter: 'blur(10px) saturate(180%)', | |
| WebkitBackdropFilter: 'blur(10px) saturate(180%)', | |
| boxShadow: isDarkMode | |
| ? '0 8px 32px 0 rgba(0, 0, 0, 0.5), 0 0 15px rgba(136, 97, 216, 0.12), 0 0 30px rgba(136, 97, 216, 0.06)' | |
| : '0 8px 32px 0 rgba(31, 38, 135, 0.37), 0 0 15px rgba(136, 97, 216, 0.2), 0 0 30px rgba(136, 97, 216, 0.12)', | |
| position: 'relative', | |
| isolation: 'isolate', | |
| boxSizing: 'border-box', | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: 'flex', | |
| justifyContent: 'flex-end', | |
| alignItems: 'center', | |
| marginBottom: '0.75rem', | |
| gap: '1rem', | |
| flexWrap: 'wrap', | |
| }} | |
| > | |
| {hint && ( | |
| <div | |
| style={{ | |
| fontSize: '0.875rem', | |
| color: 'var(--ifm-color-content-secondary)', | |
| display: 'flex', | |
| alignItems: 'flex-start', | |
| gap: '0.5rem', | |
| marginRight: 'auto', | |
| flex: '1 1 0', | |
| minWidth: '200px', | |
| }} | |
| > | |
| <svg | |
| width='20' | |
| height='32' | |
| viewBox='0 0 24 24' | |
| fill='none' | |
| stroke='currentColor' | |
| strokeWidth='2' | |
| strokeLinecap='round' | |
| strokeLinejoin='round' | |
| xmlns='http://www.w3.org/2000/svg' | |
| style={{ flexShrink: 0, alignSelf: 'flex-start' }} | |
| > | |
| <path d='M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5' /> | |
| <path d='M9 18h6' /> | |
| <path d='M10 22h4' /> | |
| </svg> | |
| <span>{hint}</span> | |
| </div> | |
| )} | |
| <div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0, marginLeft: hint ? 0 : 'auto' }}> | |
| <button | |
| onClick={toggleFullWidth} | |
| style={{ | |
| padding: '0.5rem 1rem', | |
| backgroundColor: isFullWidth ? '#059669' : '#07bc85', | |
| color: 'white', | |
| border: 'none', | |
| borderRadius: '4px', | |
| cursor: 'pointer', | |
| fontSize: '0.875rem', | |
| fontWeight: '500', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '0.5rem', | |
| }} | |
| onMouseOver={(e) => { | |
| if (!isFullWidth) { | |
| e.currentTarget.style.backgroundColor = '#059669'; | |
| } | |
| }} | |
| onMouseOut={(e) => { | |
| if (!isFullWidth) { | |
| e.currentTarget.style.backgroundColor = '#07bc85'; | |
| } | |
| }} | |
| > | |
| <svg | |
| width='16' | |
| height='16' | |
| viewBox='0 0 16 16' | |
| fill='none' | |
| xmlns='http://www.w3.org/2000/svg' | |
| style={{ flexShrink: 0 }} | |
| > | |
| {isFullWidth ? ( | |
| <path d='M3 3h10v10H3V3zm1 1v8h8V4H4zm2 2h4v4H6V6z' fill='currentColor' /> | |
| ) : ( | |
| <path d='M2 2h12v12H2V2zm1 1v10h10V3H3zm1 1h8v8H4V4z' fill='currentColor' /> | |
| )} | |
| </svg> | |
| {isFullWidth ? 'Exit Full Width' : 'Full Width'} | |
| </button> | |
| <button | |
| onClick={openInNewWindow} | |
| style={{ | |
| padding: '0.5rem 1rem', | |
| backgroundColor: '#07bc85', | |
| color: 'white', | |
| border: 'none', | |
| borderRadius: '4px', | |
| cursor: 'pointer', | |
| fontSize: '0.875rem', | |
| fontWeight: '500', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '0.5rem', | |
| }} | |
| onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#059669')} | |
| onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#07bc85')} | |
| > | |
| <svg | |
| width='16' | |
| height='16' | |
| viewBox='0 0 16 16' | |
| fill='none' | |
| xmlns='http://www.w3.org/2000/svg' | |
| style={{ flexShrink: 0 }} | |
| > | |
| <path | |
| d='M10 2h4v4M14 2l-6 6M14 6v4a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2h4' | |
| stroke='currentColor' | |
| strokeWidth='1.5' | |
| strokeLinecap='round' | |
| strokeLinejoin='round' | |
| fill='none' | |
| /> | |
| </svg> | |
| Open in New Window | |
| </button> | |
| </div> | |
| </div> | |
| <div | |
| ref={iframeWrapperRef} | |
| style={{ | |
| overflow: 'hidden', | |
| width: '100%', | |
| height: isFullWidth ? `${800 * zoom}px` : `${600 * zoom}px`, | |
| position: 'relative', | |
| borderRadius: '4px', | |
| border: isDarkMode ? '1px solid rgba(255, 255, 255, 0.15)' : '1px solid rgba(0, 0, 0, 0.1)', | |
| isolation: 'isolate', | |
| backgroundColor: 'white', | |
| boxShadow: isDarkMode | |
| ? 'inset 0 1px 2px 0 rgba(0, 0, 0, 0.2)' | |
| : 'inset 0 1px 2px 0 rgba(255, 255, 255, 0.5)', | |
| }} | |
| > | |
| <div | |
| style={{ | |
| transform: `scale(${zoom})`, | |
| transformOrigin: 'top left', | |
| width: `${100 / zoom}%`, | |
| height: `${(isFullWidth ? 800 : 600) / zoom}px`, | |
| transition: 'transform 0.2s ease', | |
| }} | |
| > | |
| <iframe | |
| src={src} | |
| style={{ | |
| width: '100%', | |
| height: isFullWidth ? '800px' : '600px', | |
| border: 'none', | |
| pointerEvents: 'auto', | |
| display: 'block', | |
| isolation: 'isolate', | |
| position: 'relative', | |
| zIndex: 1, | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment