This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from '@storybook/nextjs'
import SVGRenderer from '.'
const SAMPLE_SVG = `
<svg width="400" height="280" viewBox="0 0 400 280" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#D1E9FF"/>
<stop offset="100%" stop-color="#FBE8FF"/>
</linearGradient>
</defs>
<rect width="400" height="280" rx="24" fill="url(#bg)"/>
<g font-family="sans-serif" fill="#1F2937" text-anchor="middle">
<text x="200" y="120" font-size="32" font-weight="600">SVG Preview</text>
<text x="200" y="160" font-size="16">Click to open high-resolution preview</text>
</g>
<circle cx="320" cy="70" r="28" fill="#E0F2FE" stroke="#2563EB" stroke-width="4"/>
<circle cx="80" cy="200" r="18" fill="#FDE68A" stroke="#CA8A04" stroke-width="4"/>
<rect x="120" y="190" width="160" height="48" rx="12" fill="#FFF" opacity="0.85"/>
<text x="200" y="220" font-size="16" font-weight="500">Inline SVG asset</text>
</svg>
`.trim()
const meta = {
title: 'Base/Data Display/SVGRenderer',
component: SVGRenderer,
parameters: {
docs: {
description: {
component: 'Renders sanitized SVG markup with zoom-to-preview capability.',
},
source: {
language: 'tsx',
code: `
<SVGRenderer content={\`
<svg width="400" height="280" ...>...</svg>
\`} />
`.trim(),
},
},
},
tags: ['autodocs'],
args: {
content: SAMPLE_SVG,
},
} satisfies Meta<typeof SVGRenderer>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}

View File

@@ -0,0 +1,78 @@
import { useEffect, useRef, useState } from 'react'
import { SVG } from '@svgdotjs/svg.js'
import DOMPurify from 'dompurify'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
const SVGRenderer = ({ content }: { content: string }) => {
const svgRef = useRef<HTMLDivElement>(null)
const [imagePreview, setImagePreview] = useState('')
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
})
const svgToDataURL = (svgElement: Element): string => {
const svgString = new XMLSerializer().serializeToString(svgElement)
const base64String = Buffer.from(svgString).toString('base64')
return `data:image/svg+xml;base64,${base64String}`
}
useEffect(() => {
const handleResize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
useEffect(() => {
if (svgRef.current) {
try {
svgRef.current.innerHTML = ''
const draw = SVG().addTo(svgRef.current)
const parser = new DOMParser()
const svgDoc = parser.parseFromString(content, 'image/svg+xml')
const svgElement = svgDoc.documentElement
if (!(svgElement instanceof SVGElement))
throw new Error('Invalid SVG content')
const originalWidth = Number.parseInt(svgElement.getAttribute('width') || '400', 10)
const originalHeight = Number.parseInt(svgElement.getAttribute('height') || '600', 10)
draw.viewbox(0, 0, originalWidth, originalHeight)
svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
const rootElement = draw.svg(DOMPurify.sanitize(content))
rootElement.click(() => {
setImagePreview(svgToDataURL(svgElement as Element))
})
}
catch {
if (svgRef.current)
svgRef.current.innerHTML = '<span style="padding: 1rem;">Error rendering SVG. Wait for the image content to complete.</span>'
}
}
}, [content, windowSize])
return (
<>
<div ref={svgRef} style={{
maxHeight: '80vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
wordBreak: 'break-word',
whiteSpace: 'normal',
margin: '0 auto',
}} />
{imagePreview && (<ImagePreview url={imagePreview} title='Preview' onCancel={() => setImagePreview('')} />)}
</>
)
}
export default SVGRenderer